diff --git a/api/v1beta2/imagerepository_types.go b/api/v1beta2/imagerepository_types.go index eaee2c14..5b770a1b 100644 --- a/api/v1beta2/imagerepository_types.go +++ b/api/v1beta2/imagerepository_types.go @@ -104,6 +104,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 40075d79..44dec07f 100644 --- a/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml +++ b/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml @@ -315,6 +315,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/v1beta2/image-reflector.md b/docs/api/v1beta2/image-reflector.md index 576479fa..b215c913 100644 --- a/docs/api/v1beta2/image-reflector.md +++ b/docs/api/v1beta2/image-reflector.md @@ -543,6 +543,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.

+ + @@ -731,6 +744,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 12986aaa..34789cb4 100644 --- a/docs/spec/v1beta2/imagerepositories.md +++ b/docs/spec/v1beta2/imagerepositories.md @@ -318,6 +318,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 b92a230b..64e7b583 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/fluxcd/pkg/apis/event v0.5.2 github.com/fluxcd/pkg/apis/meta v1.1.2 github.com/fluxcd/pkg/oci v0.31.0 - github.com/fluxcd/pkg/runtime v0.42.0 + github.com/fluxcd/pkg/runtime v0.42.1-0.20231114032839-2a5dc7e6a305 github.com/fluxcd/pkg/version v0.2.2 github.com/google/go-containerregistry v0.16.1 github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20230802205906-a54d64203cff @@ -21,11 +21,11 @@ require ( github.com/onsi/gomega v1.27.10 github.com/spf13/pflag v1.0.5 go.uber.org/zap v1.25.0 - k8s.io/api v0.27.4 - k8s.io/apimachinery v0.27.4 - k8s.io/client-go v0.27.4 + k8s.io/api v0.27.7 + k8s.io/apimachinery v0.27.7 + k8s.io/client-go v0.27.7 k8s.io/utils v0.0.0-20230505201702-9f6742963106 - sigs.k8s.io/controller-runtime v0.15.1 + sigs.k8s.io/controller-runtime v0.15.3 ) require ( @@ -156,9 +156,9 @@ require ( gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.27.3 // indirect + k8s.io/apiextensions-apiserver v0.27.7 // indirect k8s.io/cli-runtime v0.27.2 // indirect - k8s.io/component-base v0.27.4 // indirect + k8s.io/component-base v0.27.7 // indirect k8s.io/klog/v2 v2.100.1 // indirect k8s.io/kube-openapi v0.0.0-20230515203736-54b630e78af5 // indirect k8s.io/kubectl v0.27.2 // indirect diff --git a/go.sum b/go.sum index a532f176..16f65131 100644 --- a/go.sum +++ b/go.sum @@ -176,8 +176,8 @@ github.com/fluxcd/pkg/apis/meta v1.1.2 h1:Unjo7hxadtB2dvGpeFqZZUdsjpRA08YYSBb7dF github.com/fluxcd/pkg/apis/meta v1.1.2/go.mod h1:BHQyRHCskGMEDf6kDGbgQ+cyiNpUHbLsCOsaMYM2maI= github.com/fluxcd/pkg/oci v0.31.0 h1:Zpp65vcFJKRfeltuswKztJh2OrB86X3VrA1LU/VjspQ= github.com/fluxcd/pkg/oci v0.31.0/go.mod h1:UL7nzm7p3fk5X0ZTsHl3qBhRy/NtuGqFSangXvPKUNw= -github.com/fluxcd/pkg/runtime v0.42.0 h1:a5DQ/f90YjoHBmiXZUpnp4bDSLORjInbmqP7K11L4uY= -github.com/fluxcd/pkg/runtime v0.42.0/go.mod h1:p6A3xWVV8cKLLQW0N90GehKgGMMmbNYv+OSJ/0qB0vg= +github.com/fluxcd/pkg/runtime v0.42.1-0.20231114032839-2a5dc7e6a305 h1:8zhGZCjqLFZUfbLP4fc893KnMv805M4DTi9VLTwZjgQ= +github.com/fluxcd/pkg/runtime v0.42.1-0.20231114032839-2a5dc7e6a305/go.mod h1:Acr6IqeAnjXs2so1m+5U25/JkZKhyLpRjfG844TbguA= 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= @@ -617,18 +617,18 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.27.4 h1:0pCo/AN9hONazBKlNUdhQymmnfLRbSZjd5H5H3f0bSs= -k8s.io/api v0.27.4/go.mod h1:O3smaaX15NfxjzILfiln1D8Z3+gEYpjEpiNA/1EVK1Y= -k8s.io/apiextensions-apiserver v0.27.3 h1:xAwC1iYabi+TDfpRhxh4Eapl14Hs2OftM2DN5MpgKX4= -k8s.io/apiextensions-apiserver v0.27.3/go.mod h1:BH3wJ5NsB9XE1w+R6SSVpKmYNyIiyIz9xAmBl8Mb+84= -k8s.io/apimachinery v0.27.4 h1:CdxflD4AF61yewuid0fLl6bM4a3q04jWel0IlP+aYjs= -k8s.io/apimachinery v0.27.4/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= +k8s.io/api v0.27.7 h1:7yG4D3t/q4utJe2ptlRw9aPuxcSmroTsYxsofkQNl/A= +k8s.io/api v0.27.7/go.mod h1:ZNExI/Lhrs9YrLgVWx6jjHZdoWCTXfBXuFjt1X6olro= +k8s.io/apiextensions-apiserver v0.27.7 h1:YqIOwZAUokzxJIjunmUd4zS1v3JhK34EPXn+pP0/bsU= +k8s.io/apiextensions-apiserver v0.27.7/go.mod h1:x0p+b5a955lfPz9gaDeBy43obM12s+N9dNHK6+dUL+g= +k8s.io/apimachinery v0.27.7 h1:Gxgtb7Y/Rsu8ymgmUEaiErkxa6RY4oTd8kNUI6SUR58= +k8s.io/apimachinery v0.27.7/go.mod h1:jBGQgTjkw99ef6q5hv1YurDd3BqKDk9YRxmX0Ozo0i8= k8s.io/cli-runtime v0.27.2 h1:9HI8gfReNujKXt16tGOAnb8b4NZ5E+e0mQQHKhFGwYw= k8s.io/cli-runtime v0.27.2/go.mod h1:9UecpyPDTkhiYY4d9htzRqN+rKomJgyb4wi0OfrmCjw= -k8s.io/client-go v0.27.4 h1:vj2YTtSJ6J4KxaC88P4pMPEQECWMY8gqPqsTgUKzvjk= -k8s.io/client-go v0.27.4/go.mod h1:ragcly7lUlN0SRPk5/ZkGnDjPknzb37TICq07WhI6Xc= -k8s.io/component-base v0.27.4 h1:Wqc0jMKEDGjKXdae8hBXeskRP//vu1m6ypC+gwErj4c= -k8s.io/component-base v0.27.4/go.mod h1:hoiEETnLc0ioLv6WPeDt8vD34DDeB35MfQnxCARq3kY= +k8s.io/client-go v0.27.7 h1:+Xgh9OOKv6A3qdD4Dnl/0VOI5EvAv+0s/OseDxVVTwQ= +k8s.io/client-go v0.27.7/go.mod h1:dZ2kqcalYp5YZ2EV12XIMc77G6PxHWOJp/kclZr4+5Q= +k8s.io/component-base v0.27.7 h1:kngM58HR9W9Nqpv7e4rpdRyWnKl/ABpUhLAZ+HoliMs= +k8s.io/component-base v0.27.7/go.mod h1:YGjlCVL1oeKvG3HSciyPHFh+LCjIEqsxz4BDR3cfHRs= k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20230515203736-54b630e78af5 h1:azYPdzztXxPSa8wb+hksEKayiz0o+PPisO/d+QhWnoo= @@ -639,8 +639,8 @@ k8s.io/utils v0.0.0-20230505201702-9f6742963106 h1:EObNQ3TW2D+WptiYXlApGNLVy0zm/ k8s.io/utils v0.0.0-20230505201702-9f6742963106/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/cli-utils v0.35.0 h1:dfSJaF1W0frW74PtjwiyoB4cwdRygbHnC7qe7HF0g/Y= sigs.k8s.io/cli-utils v0.35.0/go.mod h1:ITitykCJxP1vaj1Cew/FZEaVJ2YsTN9Q71m02jebkoE= -sigs.k8s.io/controller-runtime v0.15.1 h1:9UvgKD4ZJGcj24vefUFgZFP3xej/3igL9BsOUTb/+4c= -sigs.k8s.io/controller-runtime v0.15.1/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk= +sigs.k8s.io/controller-runtime v0.15.3 h1:L+t5heIaI3zeejoIyyvLQs5vTVu/67IU2FfisVzFlBc= +sigs.k8s.io/controller-runtime v0.15.3/go.mod h1:kp4jckA4vTx281S/0Yk2LFEEQe67mjg+ev/yknv47Ds= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kustomize/api v0.13.4 h1:E38Hfx0G9R9v7vRgKshviPotJQETG0S2gD3JdHLCAsI= diff --git a/internal/controller/imagerepository_controller.go b/internal/controller/imagerepository_controller.go index 0ae58872..4f1ea842 100644 --- a/internal/controller/imagerepository_controller.go +++ b/internal/controller/imagerepository_controller.go @@ -113,6 +113,7 @@ type ImageRepositoryReconciler struct { DatabaseReader } DeprecatedLoginOpts login.ProviderOptions + AllowInsecureHTTP bool patchOptions []patch.Option } @@ -249,9 +250,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, helper.ErrInsecureHTTPBlocked) { + reason = meta.InsecureConnectionsDisallowedReason + } else { + reason = imagev1.ImageURLInvalidReason + } + conditions.MarkStalled(obj, reason, err.Error()) result, retErr = ctrl.Result{}, nil return } @@ -268,11 +275,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, helper.ErrInsecureHTTPBlocked) { + 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. @@ -468,7 +482,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 } @@ -570,13 +584,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, helper.ErrInsecureHTTPBlocked + } + } + + 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 df25ac43..afe96c90 100644 --- a/internal/controller/imagerepository_controller_test.go +++ b/internal/controller/imagerepository_controller_test.go @@ -34,6 +34,7 @@ import ( "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/conditions" + "github.com/fluxcd/pkg/runtime/controller" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" "github.com/fluxcd/image-reflector-controller/internal/secret" @@ -580,7 +581,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{} @@ -656,12 +657,15 @@ 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 + err error + wantRef string }{ { name: "simple valid url", @@ -684,16 +688,39 @@ func TestParseImageReference(t *testing.T) { wantErr: false, wantRef: "example.com:9999/foo/bar", }, + { + name: "with insecure allowed", + url: "example.com/foo/bar", + insecure: true, + allowInsecure: true, + wantRef: "example.com/foo/bar", + }, + { + name: "with insecure disallowed", + url: "example.com/foo/bar", + insecure: true, + wantErr: true, + err: controller.ErrInsecureHTTPBlocked, + }, } 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 tt.err != nil { + g.Expect(tt.err).To(Equal(err)) + } 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 a94fadde..20fc6332 100644 --- a/main.go +++ b/main.go @@ -77,6 +77,7 @@ func main() { logOptions logger.Options leaderElectionOptions leaderelection.Options watchOptions helper.WatchOptions + connOptions helper.ConnectionOptions storagePath string storageValueLogFileSize int64 concurrent int @@ -107,11 +108,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."+ @@ -216,6 +224,7 @@ func main() { AzureAutoLogin: azureAutoLogin, GcpAutoLogin: gcpAutoLogin, }, + AllowInsecureHTTP: connOptions.AllowHTTP, }).SetupWithManager(mgr, controller.ImageRepositoryReconcilerOptions{ RateLimiter: helper.GetRateLimiter(rateLimiterOptions), }); err != nil {