diff --git a/api/v1beta2/imagerepository_types.go b/api/v1beta2/imagerepository_types.go index abfd736e..a8cf7fe7 100644 --- a/api/v1beta2/imagerepository_types.go +++ b/api/v1beta2/imagerepository_types.go @@ -98,6 +98,11 @@ type ImageRepositorySpec struct { // +kubebuilder:default:=generic // +optional Provider string `json:"provider,omitempty"` + + // Insecure, if set to true indicates that the image registry is hosted at an + // HTTP endpoint. + // +optional + Insecure bool `json:"insecure,omitempty"` } type ScanResult struct { diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml index 895d630f..2c699472 100644 --- a/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml +++ b/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml @@ -313,6 +313,10 @@ spec: image: description: Image is the name of the image repository type: string + insecure: + description: Insecure, if set to true indicates that the image registry + is hosted at an HTTP endpoint. + type: boolean interval: description: Interval is the length of time to wait between scans of the image repository. diff --git a/docs/api/image-reflector.md b/docs/api/image-reflector.md index f4eeceeb..3ff2a6ee 100644 --- a/docs/api/image-reflector.md +++ b/docs/api/image-reflector.md @@ -540,6 +540,19 @@ string When not specified, defaults to ‘generic’.

+ + +insecure
+ +bool + + + +(Optional) +

Insecure, if set to true indicates that the image registry is hosted at an +HTTP endpoint.

+ + @@ -725,6 +738,19 @@ string When not specified, defaults to ‘generic’.

+ + +insecure
+ +bool + + + +(Optional) +

Insecure, if set to true indicates that the image registry is hosted at an +HTTP endpoint.

+ + diff --git a/docs/spec/v1beta2/imagerepositories.md b/docs/spec/v1beta2/imagerepositories.md index 504452fb..b6e80c9d 100644 --- a/docs/spec/v1beta2/imagerepositories.md +++ b/docs/spec/v1beta2/imagerepositories.md @@ -297,6 +297,16 @@ spec: - "1.1.1|1.0.0" ``` +### Insecure + +`.spec.insecure` is an optional field to specify that the image registry is +hosted at a non-TLS endpoint and thus the controller should use plain HTTP +requests to communicate with the registry. + +> If an ImageRepository has `.spec.insecure` as `true` and the controller has + `--insecure-allow-http` set to `false`, then the object is marked as stalled. + For more details, see: https://github.com/fluxcd/flux2/tree/ddcc301ab6289e0640174cb9f3d46f1eeab57927/rfcs/0004-insecure-http#design-details + ### Provider `.spec.provider` is an optional field that allows specifying an OIDC provider diff --git a/go.mod b/go.mod index a59ac6bc..c7cf2680 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/fluxcd/pkg/apis/event v0.5.1 github.com/fluxcd/pkg/apis/meta v1.1.1 github.com/fluxcd/pkg/oci v0.28.0 - github.com/fluxcd/pkg/runtime v0.39.0 + github.com/fluxcd/pkg/runtime v0.40.0 github.com/fluxcd/pkg/version v0.2.2 github.com/google/go-containerregistry v0.15.2 github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20230625233257-b8504803389b diff --git a/go.sum b/go.sum index 23bf1dd7..e60e1ce1 100644 --- a/go.sum +++ b/go.sum @@ -175,8 +175,8 @@ github.com/fluxcd/pkg/apis/meta v1.1.1 h1:sLAKLbEu7rRzJ+Mytffu3NcpfdbOBTa6hcpOQz github.com/fluxcd/pkg/apis/meta v1.1.1/go.mod h1:soCfzjFWbm1mqybDcOywWKTCEYlH3skpoNGTboVk234= github.com/fluxcd/pkg/oci v0.28.0 h1:E8VvMFzU/+9vgM4IFbiwmCwaMPCq1WXPiKUmHtDVSbc= github.com/fluxcd/pkg/oci v0.28.0/go.mod h1:eFP5sQH4yWghFbcLWxdo0eI6wZ4h3HiTW0UoG33S2pg= -github.com/fluxcd/pkg/runtime v0.39.0 h1:vgmzYS+DT0w8ikX9MqGsOdmMagoiKys2RMGdl/EDbgc= -github.com/fluxcd/pkg/runtime v0.39.0/go.mod h1:0A/0kZv/MPciAj5AoSEDKVeqUFEF6371q7o+zk6l81g= +github.com/fluxcd/pkg/runtime v0.40.0 h1:uGiiEbMZwd7xmbKaVmcH7iilCFW9betWbz0r1taK3G0= +github.com/fluxcd/pkg/runtime v0.40.0/go.mod h1:BqHEOVrZmt19p0q1OlGFWAYh3rZ28+IBpxLB2yPjjQ4= github.com/fluxcd/pkg/version v0.2.2 h1:ZpVXECeLA5hIQMft11iLp6gN3cKcz6UNuVTQPw/bRdI= github.com/fluxcd/pkg/version v0.2.2/go.mod h1:NGnh/no8S6PyfCDxRFrPY3T5BUnqP48MxfxNRU0z8C0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= diff --git a/internal/controller/imagerepository_controller.go b/internal/controller/imagerepository_controller.go index d5e6d2f0..19fdad86 100644 --- a/internal/controller/imagerepository_controller.go +++ b/internal/controller/imagerepository_controller.go @@ -86,6 +86,10 @@ const ( scanReasonInterval = "triggered by interval" ) +// insecureHTTPError occurs when insecure HTTP communication is tried +// and such behaviour is blocked. +var insecureHTTPError = errors.New("use of insecure plain HTTP connections is blocked") + // getPatchOptions composes patch options based on the given parameters. // It is used as the options used when patching an object. func getPatchOptions(ownedConditions []string, controllerName string) []patch.Option { @@ -113,6 +117,7 @@ type ImageRepositoryReconciler struct { DatabaseReader } DeprecatedLoginOpts login.ProviderOptions + AllowInsecureHTTP bool patchOptions []patch.Option } @@ -249,9 +254,15 @@ func (r *ImageRepositoryReconciler) reconcile(ctx context.Context, sp *patch.Ser } // Parse image reference. - ref, err := parseImageReference(obj.Spec.Image) + ref, err := r.parseImageReference(obj.Spec.Image, obj.Spec.Insecure) if err != nil { - conditions.MarkStalled(obj, imagev1.ImageURLInvalidReason, err.Error()) + var reason string + if errors.Is(err, insecureHTTPError) { + reason = meta.InsecureConnectionsDisallowedReason + } else { + reason = imagev1.ImageURLInvalidReason + } + conditions.MarkStalled(obj, reason, err.Error()) result, retErr = ctrl.Result{}, nil return } @@ -268,11 +279,18 @@ func (r *ImageRepositoryReconciler) reconcile(ctx context.Context, sp *patch.Ser // Check if it can be scanned now. ok, when, reasonMsg, err := r.shouldScan(*obj, startTime) if err != nil { - e := fmt.Errorf("failed to determine if it's scan time: %w", err) - conditions.MarkFalse(obj, meta.ReadyCondition, metav1.StatusFailure, e.Error()) + var e error + if errors.Is(err, insecureHTTPError) { + e = err + conditions.MarkStalled(obj, meta.InsecureConnectionsDisallowedReason, e.Error()) + } else { + e = fmt.Errorf("failed to determine if it's scan time: %w", err) + conditions.MarkFalse(obj, meta.ReadyCondition, metav1.StatusFailure, e.Error()) + } result, retErr = ctrl.Result{}, e return } + conditions.Delete(obj, meta.StalledCondition) // Scan the repository if it's scan time. No scan is a no-op reconciliation. // The next scan time is not reset in case of no-op reconciliation. @@ -458,7 +476,7 @@ func (r *ImageRepositoryReconciler) shouldScan(obj imagev1.ImageRepository, now // If the canonical image name of the image is different from the last // observed name, scan now. - ref, err := parseImageReference(obj.Spec.Image) + ref, err := r.parseImageReference(obj.Spec.Image, obj.Spec.Insecure) if err != nil { return false, scanInterval, "", err } @@ -560,13 +578,23 @@ func eventLogf(ctx context.Context, r kuberecorder.EventRecorder, obj runtime.Ob } // parseImageReference parses the given URL into a container registry repository -// reference. -func parseImageReference(url string) (name.Reference, error) { +// reference. If insecure is set to true, then the registry is deemed to be +// located at an HTTP endpoint. +func (r *ImageRepositoryReconciler) parseImageReference(url string, insecure bool) (name.Reference, error) { if s := strings.Split(url, "://"); len(s) > 1 { return nil, fmt.Errorf(".spec.image value should not start with URL scheme; remove '%s://'", s[0]) } - ref, err := name.ParseReference(url) + var opts []name.Option + if insecure { + if r.AllowInsecureHTTP { + opts = append(opts, name.Insecure) + } else { + return nil, insecureHTTPError + } + } + + ref, err := name.ParseReference(url, opts...) if err != nil { return nil, err } diff --git a/internal/controller/imagerepository_controller_test.go b/internal/controller/imagerepository_controller_test.go index 81eddfb3..9213da5b 100644 --- a/internal/controller/imagerepository_controller_test.go +++ b/internal/controller/imagerepository_controller_test.go @@ -527,7 +527,7 @@ func TestImageRepositoryReconciler_scan(t *testing.T) { repo.SetAnnotations(map[string]string{meta.ReconcileRequestAnnotation: tt.annotation}) } - ref, err := parseImageReference(imgRepo) + ref, err := r.parseImageReference(imgRepo, false) g.Expect(err).ToNot(HaveOccurred()) opts := []remote.Option{} @@ -603,12 +603,14 @@ func TestGetLatestTags(t *testing.T) { } } -func TestParseImageReference(t *testing.T) { +func Test_parseImageReference(t *testing.T) { tests := []struct { - name string - url string - wantErr bool - wantRef string + name string + url string + insecure bool + allowInsecure bool + wantErr bool + wantRef string }{ { name: "simple valid url", @@ -631,16 +633,37 @@ func TestParseImageReference(t *testing.T) { wantErr: false, wantRef: "example.com:9999/foo/bar", }, + { + name: "with allowed insecure", + url: "example.com/foo/bar", + insecure: true, + allowInsecure: true, + wantErr: false, + wantRef: "example.com/foo/bar", + }, + { + name: "with disallowed insecure", + url: "example.com/foo/bar", + insecure: true, + allowInsecure: false, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - ref, err := parseImageReference(tt.url) + r := &ImageRepositoryReconciler{ + AllowInsecureHTTP: tt.allowInsecure, + } + ref, err := r.parseImageReference(tt.url, tt.insecure) g.Expect(err != nil).To(Equal(tt.wantErr)) if err == nil { g.Expect(ref.String()).To(Equal(tt.wantRef)) + if tt.insecure { + g.Expect(ref.Context().Registry.Scheme()).To(Equal("http")) + } } }) } diff --git a/main.go b/main.go index 656a21b6..7605a3bb 100644 --- a/main.go +++ b/main.go @@ -76,6 +76,7 @@ func main() { logOptions logger.Options leaderElectionOptions leaderelection.Options watchOptions helper.WatchOptions + connOptions helper.ConnectionOptions storagePath string storageValueLogFileSize int64 concurrent int @@ -106,11 +107,18 @@ func main() { rateLimiterOptions.BindFlags(flag.CommandLine) featureGates.BindFlags(flag.CommandLine) watchOptions.BindFlags(flag.CommandLine) + connOptions.BindFlags(flag.CommandLine) flag.Parse() logger.SetLogger(logger.NewLogger(logOptions)) + if err := connOptions.CheckEnvironmentCompatibility(); err != nil { + setupLog.Error(err, + "please verify that your controller flag settings are compatible with the controller's environment") + os.Exit(1) + } + if awsAutoLogin || gcpAutoLogin || azureAutoLogin { setupLog.Error(errors.New("use of deprecated flags"), "autologin flags have been deprecated. These flags will be removed in a future release."+ @@ -215,6 +223,7 @@ func main() { AzureAutoLogin: azureAutoLogin, GcpAutoLogin: gcpAutoLogin, }, + AllowInsecureHTTP: connOptions.AllowHTTP, }).SetupWithManager(mgr, controller.ImageRepositoryReconcilerOptions{ RateLimiter: helper.GetRateLimiter(rateLimiterOptions), }); err != nil { diff --git a/podinfo-registry.yaml b/podinfo-registry.yaml new file mode 100644 index 00000000..fc7824d4 --- /dev/null +++ b/podinfo-registry.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: image.toolkit.fluxcd.io/v1beta1 +kind: ImageRepository +metadata: + name: podinfo +spec: + image: ghcr.io/stefanprodan/podinfo + interval: 1m0s + exclusionList: + - "^.*\\.4$"