Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions internal/groundcontrol/server/cached_images_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
"math"
"net/http"
"net/http/httptest"
"testing"
Expand Down Expand Up @@ -264,6 +265,33 @@ func TestSyncHandler_InvalidBody(t *testing.T) {
require.Equal(t, http.StatusBadRequest, rr.Code)
}

func TestSyncHandler_RejectsMetricsAboveMaxInt64(t *testing.T) {
tests := []struct {
name string
req SatelliteStatusRequest
}{
{name: "memory used bytes", req: SatelliteStatusRequest{MemoryUsedBytes: math.MaxInt64 + 1}},
{name: "storage used bytes", req: SatelliteStatusRequest{StorageUsedBytes: math.MaxInt64 + 1}},
{name: "last sync duration", req: SatelliteStatusRequest{LastSyncDurationMs: math.MaxInt64 + 1}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server, mock := newMockServer(t)
body := mustMarshalJSON(t, tt.req)
req := httptest.NewRequest(http.MethodPost, "/satellites/sync", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")

rr := httptest.NewRecorder()
server.SyncSatellite(rr, req)

require.Equal(t, http.StatusBadRequest, rr.Code)
require.Contains(t, rr.Body.String(), "status metrics exceed the maximum supported value")
require.NoError(t, mock.ExpectationsWereMet())
})
}
}

func TestSyncHandler_InvalidHeartbeatInterval(t *testing.T) {
server, mock := newMockServer(t)

Expand Down
4 changes: 2 additions & 2 deletions internal/groundcontrol/server/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,8 @@ func toNullString(s string) sql.NullString {
return sql.NullString{String: s, Valid: s != ""}
}

func toNullInt64(n int64) sql.NullInt64 {
return sql.NullInt64{Int64: n, Valid: true}
func toNullUInt64(n uint64) sql.NullInt64 {
return sql.NullInt64{Int64: int64(n), Valid: true}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func toNullInt32(n int32) sql.NullInt32 {
Expand Down
16 changes: 13 additions & 3 deletions internal/groundcontrol/server/satellite_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"database/sql"
"fmt"
"log"
"math"
"net/http"
"strconv"
"time"
Expand Down Expand Up @@ -606,6 +607,15 @@ func (s *Server) SyncSatellite(w http.ResponseWriter, r *http.Request) {
HandleAppError(w, err)
return
}
if req.MemoryUsedBytes > math.MaxInt64 ||
req.StorageUsedBytes > math.MaxInt64 ||
req.LastSyncDurationMs > math.MaxInt64 {
HandleAppError(w, &AppError{
Message: "status metrics exceed the maximum supported value",
Code: http.StatusBadRequest,
})
return
}

// Check SPIFFE identity first for dual auth
var satelliteName string
Expand Down Expand Up @@ -671,9 +681,9 @@ func (s *Server) SyncSatellite(w http.ResponseWriter, r *http.Request) {
LatestStateDigest: toNullString(req.LatestStateDigest),
LatestConfigDigest: toNullString(req.LatestConfigDigest),
CpuPercent: toNullString(fmt.Sprintf("%.2f", req.CPUPercent)),
MemoryUsedBytes: toNullInt64(req.MemoryUsedBytes),
StorageUsedBytes: toNullInt64(req.StorageUsedBytes),
LastSyncDurationMs: toNullInt64(req.LastSyncDurationMs),
MemoryUsedBytes: toNullUInt64(req.MemoryUsedBytes),
StorageUsedBytes: toNullUInt64(req.StorageUsedBytes),
LastSyncDurationMs: toNullUInt64(req.LastSyncDurationMs),
ImageCount: toNullInt32(req.ImageCount),
ReportedAt: req.RequestCreatedTime,
ArtifactIds: artifactIDs,
Expand Down
6 changes: 3 additions & 3 deletions internal/groundcontrol/server/server.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 8 additions & 12 deletions internal/satellite/state/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,9 @@ import (
v1 "github.com/google/go-containerregistry/pkg/v1"

"github.com/container-registry/harbor-satellite/internal/logger"
"github.com/container-registry/harbor-satellite/pkg/groundcontrol"
)

type CachedImage struct {
Reference string `json:"reference"`
SizeBytes int64 `json:"size_bytes"`
}

type catalogResponse struct {
Repositories []string `json:"repositories"`
}
Expand All @@ -28,7 +24,7 @@ type tagsResponse struct {
Tags []string `json:"tags"`
}

func collectCachedImages(ctx context.Context, registryHost string, insecure bool) ([]CachedImage, error) {
func collectCachedImages(ctx context.Context, registryHost string, insecure bool) ([]groundcontrol.CachedImageReport, error) {
log := logger.FromContext(ctx)
client := &http.Client{Timeout: 30 * time.Second}

Expand All @@ -37,7 +33,7 @@ func collectCachedImages(ctx context.Context, registryHost string, insecure bool
return nil, fmt.Errorf("fetch catalog: %w", err)
}

var images []CachedImage
var images []groundcontrol.CachedImageReport
for _, repo := range repos {
tags, err := fetchTags(ctx, client, registryHost, repo, insecure)
if err != nil {
Expand All @@ -56,31 +52,31 @@ func collectCachedImages(ctx context.Context, registryHost string, insecure bool
}

if images == nil {
return []CachedImage{}, nil
return []groundcontrol.CachedImageReport{}, nil
}

return images, nil
}

func collectImageInfo(ref string, ctxOpt crane.Option, insecure bool) (CachedImage, error) {
func collectImageInfo(ref string, ctxOpt crane.Option, insecure bool) (groundcontrol.CachedImageReport, error) {
opts := []crane.Option{ctxOpt}
if insecure {
opts = append(opts, crane.Insecure)
}

raw, err := crane.Manifest(ref, opts...)
if err != nil {
return CachedImage{}, fmt.Errorf("get manifest for %s: %w", ref, err)
return groundcontrol.CachedImageReport{}, fmt.Errorf("get manifest for %s: %w", ref, err)
}

size, err := computeManifestSize(raw)
if err != nil {
return CachedImage{}, fmt.Errorf("compute size for %s: %w", ref, err)
return groundcontrol.CachedImageReport{}, fmt.Errorf("compute size for %s: %w", ref, err)
}

digest := fmt.Sprintf("sha256:%x", sha256.Sum256(raw))

return CachedImage{
return groundcontrol.CachedImageReport{
Reference: ref + "@" + digest,
SizeBytes: size,
}, nil
Expand Down
21 changes: 21 additions & 0 deletions internal/satellite/state/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package state

import (
"fmt"
"strings"

"github.com/container-registry/harbor-satellite/internal/utils"
"github.com/container-registry/harbor-satellite/pkg/config"
"github.com/container-registry/harbor-satellite/pkg/groundcontrol"
"github.com/rs/zerolog"
)

Expand All @@ -21,3 +23,22 @@ func getStateFetcherForInputWithTLS(input, username, password string, useInsecur

return NewURLStateFetcherWithTLS(input, username, password, useInsecure, tlsCfg), nil
}

func stateConfigFromResponse(response groundcontrol.StateConfigResponse) config.StateConfig {
return config.StateConfig{
RegistryCredentials: config.RegistryCredentials{
URL: config.URL(response.Auth.URL),
Username: response.Auth.Username,
Password: response.Auth.Password,
},
StateURL: response.State,
}
}

func responseError(operation, status string, response *groundcontrol.AppError) error {
return fmt.Errorf("%s: %s: code=%d message=%q", operation, status, response.Code, response.Message)
}

func unknownResponseError(operation, status string, body []byte) error {
return fmt.Errorf("%s: unexpected response status=%q body=%q", operation, status, strings.TrimSpace(string(body)))
}
75 changes: 32 additions & 43 deletions internal/satellite/state/registration_process.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package state

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"strings"
Expand All @@ -14,14 +12,10 @@ import (
"github.com/container-registry/harbor-satellite/internal/logger"
satTLS "github.com/container-registry/harbor-satellite/internal/satellite/tls"
"github.com/container-registry/harbor-satellite/pkg/config"
"github.com/container-registry/harbor-satellite/pkg/groundcontrol"
"github.com/rs/zerolog"
)

const (
ZeroTouchRegistrationRoute = "satellites/ztr"
ZeroTouchRegistrationEventName = "zero-touch-registration-event"
)

type ZtrProcess struct {
// Name is the name of the process
name string
Expand Down Expand Up @@ -63,7 +57,6 @@ func (z *ZtrProcess) Execute(ctx context.Context) error {
// Register the satellite
stateConfig, err := registerSatellite(
gcURL,
ZeroTouchRegistrationRoute,
z.cm.GetToken(),
z.cm.GetTLSConfig(),
z.cm.UseUnsecure(),
Expand Down Expand Up @@ -199,43 +192,41 @@ func sanitizeAuditReason(err error, token string) string {
return s
}

func registerSatellite(groundControlURL, path, token string, tlsCfg config.TLSConfig, useUnsecure bool, ctx context.Context) (config.StateConfig, error) {
ztrURL := fmt.Sprintf("%s/%s", groundControlURL, path)
body, err := json.Marshal(map[string]string{"token": token})
if err != nil {
return config.StateConfig{}, fmt.Errorf("failed to encode request: %w", err)
}

client, err := createHTTPClient(tlsCfg, useUnsecure)
func registerSatellite(groundControlURL, token string, tlsCfg config.TLSConfig, useUnsecure bool, ctx context.Context) (config.StateConfig, error) {
httpClient, err := createHTTPClient(tlsCfg, useUnsecure)
if err != nil {
return config.StateConfig{}, fmt.Errorf("failed to create HTTP client: %w", err)
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, ztrURL, bytes.NewReader(body))
if err != nil {
return config.StateConfig{}, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
response, err := client.Do(req)
client, err := groundcontrol.NewClientWithResponses(
groundControlURL,
groundcontrol.WithHTTPClient(httpClient),
)
if err != nil {
return config.StateConfig{}, fmt.Errorf("failed to send request: %w", err)
return config.StateConfig{}, fmt.Errorf("failed to create Ground Control client: %w", err)
}
defer func() {
if err := response.Body.Close(); err != nil {
logger.FromContext(ctx).Warn().Err(err).Msg("error closing response body")
}
}()

if response.StatusCode != http.StatusOK {
return config.StateConfig{}, fmt.Errorf("failed to register satellite: %s", response.Status)
response, err := client.ZtrWithResponse(ctx, groundcontrol.ZTRRequest{Token: token})
if err != nil {
return config.StateConfig{}, fmt.Errorf("failed to send registration request: %w", err)
}

var authResponse config.StateConfig
if err := json.NewDecoder(response.Body).Decode(&authResponse); err != nil {
return config.StateConfig{}, fmt.Errorf("failed to decode response: %w", err)
switch {
case response.JSON200 != nil:
return stateConfigFromResponse(*response.JSON200), nil
case response.JSON400 != nil:
return config.StateConfig{}, responseError("failed to register satellite", response.Status(), response.JSON400)
case response.JSON401 != nil:
return config.StateConfig{}, responseError("failed to register satellite", response.Status(), response.JSON401)
case response.JSON422 != nil:
return config.StateConfig{}, responseError("failed to register satellite", response.Status(), response.JSON422)
case response.JSON429 != nil:
return config.StateConfig{}, responseError("failed to register satellite", response.Status(), response.JSON429)
case response.JSON500 != nil:
return config.StateConfig{}, responseError("failed to register satellite", response.Status(), response.JSON500)
default:
return config.StateConfig{}, unknownResponseError("failed to register satellite", response.Status(), response.Body)
}

return authResponse, nil
}

func createHTTPClient(tlsCfg config.TLSConfig, useUnsecure bool) (*http.Client, error) {
Expand All @@ -247,23 +238,21 @@ func createHTTPClient(tlsCfg config.TLSConfig, useUnsecure bool) (*http.Client,

if useUnsecure {
transport.TLSClientConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: true, //nolint:gosec // Explicitly enabled by use_unsecure.
}
transport.TLSClientConfig.InsecureSkipVerify = useUnsecure
} else if tlsCfg.CertFile != "" || tlsCfg.CAFile != "" {
cfg := &satTLS.Config{
} else if tlsCfg.CertFile != "" || tlsCfg.CAFile != "" || tlsCfg.SkipVerify {
loadedTLSConfig, err := satTLS.LoadClientTLSConfig(&satTLS.Config{
CertFile: tlsCfg.CertFile,
KeyFile: tlsCfg.KeyFile,
CAFile: tlsCfg.CAFile,
SkipVerify: tlsCfg.SkipVerify,
MinVersion: tls.VersionTLS12,
}

tlsConfig, err := satTLS.LoadClientTLSConfig(cfg)
})
if err != nil {
return nil, fmt.Errorf("load TLS config: %w", err)
}
transport.TLSClientConfig = tlsConfig
transport.TLSClientConfig = loadedTLSConfig
}

return &http.Client{
Expand Down
Loading
Loading