This is an automated email from the ASF dual-hosted git repository. squakez pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-k.git
commit 06366afacb4a02ce7ec2fb3e2816740469fc54e2 Author: Pasquale Congiusti <[email protected]> AuthorDate: Sat Sep 19 12:50:27 2026 +0200 feat(install): support development container registry Closes #6758 --- pkg/cmd/operator/operator.go | 20 ++ pkg/install/registry.go | 215 +++++++++++++++++++++ .../config/manager/operator-deployment.yaml | 3 + ...ization.yaml => dev-registry-role-binding.yaml} | 26 +-- .../{kustomization.yaml => dev-registry-role.yaml} | 26 +-- pkg/resources/config/rbac/kustomization.yaml | 2 + pkg/resources/resources/registry/deploy.yaml | 81 ++++++++ .../registry/secret.yaml} | 21 +- .../registry/service.yaml} | 29 +-- pkg/util/kubernetes/docker_secret.go | 110 +++++++++++ 10 files changed, 481 insertions(+), 52 deletions(-) diff --git a/pkg/cmd/operator/operator.go b/pkg/cmd/operator/operator.go index 2d34ea496..60b181bf8 100644 --- a/pkg/cmd/operator/operator.go +++ b/pkg/cmd/operator/operator.go @@ -239,6 +239,26 @@ func Run(healthPort, monitoringPort int32, leaderElection bool, leaderElectionID defer installCancel() install.OperatorStartupOptionalTools(installCtx, bootstrapClient, log) + devRegistryEnvVal, devReg := os.LookupEnv("ENABLE_DEV_REGISTRY") + if devReg && devRegistryEnvVal == "true" { + // Only enable registry protected by secret if configured + devRegistrySecretEnvVal, devRegSecret := os.LookupEnv("ENABLE_DEV_REGISTRY_SECRET") + withSecret := devRegSecret && devRegistrySecretEnvVal == "true" + log.Info("Installing development container registry") + if withSecret { + log.Info("NOTE: ENABLE_DEV_REGISTRY_SECRET is set, mind to provide a secret with the default values in the Integration namespace " + + "and to pull images with it.") + } + log.Info("WARNING: the internal development container registry is ephemeral and not secured. " + + "It MUST be considered for DEVELOPMENT and DEMO purposes only. Make sure to read documentation and switch to " + + "a production grade container registry when moving the operator to a production environment.") + registryCtx, registryCancel := context.WithTimeout(ctx, 1*time.Minute) + defer registryCancel() + if err := install.OperatorStartupRegistry(registryCtx, bootstrapClient, withSecret); err != nil { + log.Error(err, "could not install the development container registry") + } + } + log.Info("Starting the manager") exitOnError(mgr.Start(ctx), "manager exited non-zero") } diff --git a/pkg/install/registry.go b/pkg/install/registry.go new file mode 100644 index 000000000..460b6a740 --- /dev/null +++ b/pkg/install/registry.go @@ -0,0 +1,215 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package install + +import ( + "context" + "fmt" + "os" + "os/exec" + "time" + + "github.com/apache/camel-k/v2/pkg/client" + "github.com/apache/camel-k/v2/pkg/platform" + "github.com/apache/camel-k/v2/pkg/resources" + "github.com/apache/camel-k/v2/pkg/util/kubernetes" + "github.com/apache/camel-k/v2/pkg/util/log" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/yaml" +) + +// OperatorStartupRegistry tries to install optional development container registry. +func OperatorStartupRegistry(ctx context.Context, c client.Client, withSecret bool) error { + secret, err := resources.Resource("/resources/registry/secret.yaml") + if err != nil { + return fmt.Errorf("could not load development container registry Secret configuration: %w", err) + } + var registrySecret corev1.Secret + if err = yaml.Unmarshal(secret, ®istrySecret); err != nil { + return fmt.Errorf("could not parse development container registry Secret configuration: %w", err) + } + + service, err := resources.Resource("/resources/registry/service.yaml") + if err != nil { + return fmt.Errorf("could not load development container registry Service configuration: %w", err) + } + var registryService corev1.Service + if err = yaml.Unmarshal(service, ®istryService); err != nil { + return fmt.Errorf("could not parse development container registry Service configuration: %w", err) + } + + deploy, err := resources.Resource("/resources/registry/deploy.yaml") + if err != nil { + return fmt.Errorf("could not load development container registry Deployment configuration: %w", err) + } + var registryDeploy appsv1.Deployment + if err = yaml.Unmarshal(deploy, ®istryDeploy); err != nil { + return fmt.Errorf("could not parse development container registry Deployment configuration: %w", err) + } + + if withSecret && len(registryDeploy.Spec.Template.Spec.Containers) > 0 { + registryDeploy.Spec.Template.Spec.Containers[0].Env = + append(registryDeploy.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ + Name: "REGISTRY_AUTH", + Value: "htpasswd", + }) + registryDeploy.Spec.Template.Spec.Containers[0].Env = + append(registryDeploy.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ + Name: "REGISTRY_AUTH_HTPASSWD_REALM", + Value: "Registry Realm", + }) + registryDeploy.Spec.Template.Spec.Containers[0].Env = + append(registryDeploy.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ + Name: "REGISTRY_AUTH_HTPASSWD_PATH", + Value: "/auth/htpasswd", + }) + } + + // Get owner reference to operator deployment to manage garbage collection + ref, err := getOwnerRef(ctx, c) + if err != nil { + return fmt.Errorf("could not get operator deployment ownership: %w", err) + } + + // create self signed certificate + cmd := exec.Command("openssl", + "req", "-x509", "-newkey", "rsa:2048", "-nodes", + "-keyout", "/tmp/registry.key", + "-out", "/tmp/registry.crt", + "-days", "365", + "-subj", "/CN=registry", + ) + + if err = cmd.Run(); err != nil { + return fmt.Errorf("could not generate development container registry self signed certificate: %w", err) + } + crtSecret, err := kubernetes.TLSSecretFromFiles(ctx, "camel-k", "registry-tls", "/tmp/registry.crt", "/tmp/registry.key") + if err != nil { + return fmt.Errorf("could not generate development container registry self signed certificate secret: %w", err) + } + crtSecret.SetOwnerReferences([]metav1.OwnerReference{*ref}) + if err := c.Create(ctx, crtSecret); err != nil { + return fmt.Errorf("could not create development container registry push secret: %w", err) + } + + registrySecret.SetNamespace("camel-k") + registryService.SetNamespace("camel-k") + registryDeploy.SetNamespace("camel-k") + registrySecret.SetOwnerReferences([]metav1.OwnerReference{*ref}) + registryService.SetOwnerReferences([]metav1.OwnerReference{*ref}) + registryDeploy.SetOwnerReferences([]metav1.OwnerReference{*ref}) + + // Try to create the resources now + if err := c.Create(ctx, ®istrySecret); err != nil { + return fmt.Errorf("could not create development container registry Secret configuration: %w", err) + } + if err := c.Create(ctx, ®istryService); err != nil { + return fmt.Errorf("could not create development container registry Service configuration: %w", err) + } + if err := c.Create(ctx, ®istryDeploy); err != nil { + return fmt.Errorf("could not create development container registry Deployment configuration: %w", err) + } + + // Get the cluster IP and use it to configure internally the operator + clusterIP, err := waitForClusterIP(ctx, c, registryService.GetNamespace(), registryService.GetName(), 30*time.Second) + if err != nil { + return fmt.Errorf("could not get development container registry Service IP: %w", err) + } + + dockerRegistrySecret, err := kubernetes.DockerRegistrySecret(ctx, "camel-k", "ck-dev-registry", clusterIP, "admin", "password") + if err != nil { + return fmt.Errorf("could not generate development container registry push secret: %w", err) + } + dockerRegistrySecret.SetOwnerReferences([]metav1.OwnerReference{*ref}) + if err := c.Create(ctx, dockerRegistrySecret); err != nil { + return fmt.Errorf("could not create development container registry push secret: %w", err) + } + + log.Infof("Setting up development container registry configuration environment variables (registry IP %s). Notice that it overrides the operator configuration"+ + " but it won't override any IntegrationProfile configuration.", clusterIP) + os.Setenv("REGISTRY_ADDRESS", clusterIP) + os.Setenv("REGISTRY_INSECURE", "false") + os.Setenv("REGISTRY_SECRET", dockerRegistrySecret.GetName()) + // We must reinitialize to get those values just changed in the default platform configuration + platform.InitPlatform() + + return nil +} + +func getOwnerRef(ctx context.Context, c client.Client) (*metav1.OwnerReference, error) { + operatorDeploy := &appsv1.Deployment{} + + err := c.Get(ctx, types.NamespacedName{ + // TODO: change theme + Name: "camel-k-operator", + Namespace: "camel-k", + }, operatorDeploy) + if err != nil { + return nil, err + } + + ownerRef := metav1.OwnerReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: operatorDeploy.Name, + UID: operatorDeploy.UID, + Controller: new(true), + BlockOwnerDeletion: new(true), + } + + return &ownerRef, nil +} + +func waitForClusterIP(ctx context.Context, c client.Client, namespace, name string, timeout time.Duration) (string, error) { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + + for { + var svc corev1.Service + + err := c.Get(ctx, types.NamespacedName{ + Namespace: namespace, + Name: name, + }, &svc) + if err != nil { + return "", err + } + + if svc.Spec.ClusterIP != "" && + svc.Spec.ClusterIP != corev1.ClusterIPNone { + return svc.Spec.ClusterIP, nil + } + + select { + case <-ctx.Done(): + return "", fmt.Errorf( + "timed out waiting for Service %s/%s to get a ClusterIP: %w", + namespace, + name, + ctx.Err(), + ) + case <-ticker.C: + } + } +} diff --git a/pkg/resources/config/manager/operator-deployment.yaml b/pkg/resources/config/manager/operator-deployment.yaml index 771d21d53..548b84999 100644 --- a/pkg/resources/config/manager/operator-deployment.yaml +++ b/pkg/resources/config/manager/operator-deployment.yaml @@ -81,6 +81,9 @@ spec: # You can provide a different SA for builder Pods - name: BUILDER_SA value: "camel-k-builder" + # Demo only: turn it off or remove the variable in a production environment. + - name: ENABLE_DEV_REGISTRY + value: "true" # Attempt to read bootstrap configuration from configmap or secret envFrom: - configMapRef: diff --git a/pkg/resources/config/rbac/kustomization.yaml b/pkg/resources/config/rbac/dev-registry-role-binding.yaml similarity index 74% copy from pkg/resources/config/rbac/kustomization.yaml copy to pkg/resources/config/rbac/dev-registry-role-binding.yaml index d6a983f2c..61a32dc31 100644 --- a/pkg/resources/config/rbac/kustomization.yaml +++ b/pkg/resources/config/rbac/dev-registry-role-binding.yaml @@ -15,16 +15,16 @@ # limitations under the License. # --------------------------------------------------------------------------- -# -# rbac resources applicable for all kubernetes platforms -# -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: -- builder-role.yaml -- builder-role-openshift.yaml -- kamelets-viewer-role.yaml -- builder-role-binding.yaml -- builder-role-binding-openshift.yaml -- kamelets-viewer-role-binding.yaml +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: camel-k-dev-registry + labels: + app: "camel-k" +subjects: +- kind: ServiceAccount + name: camel-k-operator +roleRef: + kind: Role + name: camel-k-dev-registry + apiGroup: rbac.authorization.k8s.io diff --git a/pkg/resources/config/rbac/kustomization.yaml b/pkg/resources/config/rbac/dev-registry-role.yaml similarity index 74% copy from pkg/resources/config/rbac/kustomization.yaml copy to pkg/resources/config/rbac/dev-registry-role.yaml index d6a983f2c..deac37f64 100644 --- a/pkg/resources/config/rbac/kustomization.yaml +++ b/pkg/resources/config/rbac/dev-registry-role.yaml @@ -15,16 +15,16 @@ # limitations under the License. # --------------------------------------------------------------------------- -# -# rbac resources applicable for all kubernetes platforms -# -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: -- builder-role.yaml -- builder-role-openshift.yaml -- kamelets-viewer-role.yaml -- builder-role-binding.yaml -- builder-role-binding-openshift.yaml -- kamelets-viewer-role-binding.yaml +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: camel-k-dev-registry + labels: + app: "camel-k" +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create diff --git a/pkg/resources/config/rbac/kustomization.yaml b/pkg/resources/config/rbac/kustomization.yaml index d6a983f2c..629776651 100644 --- a/pkg/resources/config/rbac/kustomization.yaml +++ b/pkg/resources/config/rbac/kustomization.yaml @@ -24,7 +24,9 @@ kind: Kustomization resources: - builder-role.yaml - builder-role-openshift.yaml +- dev-registry-role.yaml - kamelets-viewer-role.yaml - builder-role-binding.yaml - builder-role-binding-openshift.yaml +- dev-registry-role-binding.yaml - kamelets-viewer-role-binding.yaml diff --git a/pkg/resources/resources/registry/deploy.yaml b/pkg/resources/resources/registry/deploy.yaml new file mode 100644 index 000000000..51d8e61d1 --- /dev/null +++ b/pkg/resources/resources/registry/deploy.yaml @@ -0,0 +1,81 @@ +# --------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# --------------------------------------------------------------------------- + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: registry +spec: + selector: + matchLabels: + app: registry + template: + metadata: + labels: + app: registry + spec: + containers: + - name: registry + image: registry:3 + env: + - name: REGISTRY_HTTP_ADDR + value: 0.0.0.0:443 + - name: REGISTRY_HTTP_TLS_CERTIFICATE + value: /certs/tls.crt + - name: REGISTRY_HTTP_TLS_KEY + value: /certs/tls.key + - name: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY + value: /registry-data + # Required to include a secret credential + # The operator will be in charge to turn them on if + # configured + # - name: REGISTRY_AUTH + # value: htpasswd + # - name: REGISTRY_AUTH_HTPASSWD_REALM + # value: Registry Realm + # - name: REGISTRY_AUTH_HTPASSWD_PATH + # value: /auth/htpasswd + ports: + - containerPort: 5000 + name: registry + - containerPort: 443 + name: https + volumeMounts: + - name: registry-data + mountPath: /registry-data + - name: registry-auth + mountPath: /auth + readOnly: true + - name: registry-tls + mountPath: /certs + readOnly: true + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + volumes: + - name: registry-data + emptyDir: {} + - name: registry-auth + secret: + secretName: registry-auth + - name: registry-tls + secret: + secretName: registry-tls diff --git a/pkg/resources/config/rbac/kustomization.yaml b/pkg/resources/resources/registry/secret.yaml similarity index 74% copy from pkg/resources/config/rbac/kustomization.yaml copy to pkg/resources/resources/registry/secret.yaml index d6a983f2c..e2c414b18 100644 --- a/pkg/resources/config/rbac/kustomization.yaml +++ b/pkg/resources/resources/registry/secret.yaml @@ -15,16 +15,11 @@ # limitations under the License. # --------------------------------------------------------------------------- -# -# rbac resources applicable for all kubernetes platforms -# -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: -- builder-role.yaml -- builder-role-openshift.yaml -- kamelets-viewer-role.yaml -- builder-role-binding.yaml -- builder-role-binding-openshift.yaml -- kamelets-viewer-role-binding.yaml +apiVersion: v1 +kind: Secret +metadata: + name: registry-auth +type: Opaque +stringData: + htpasswd: | + admin:$2y$05$b8N7UpPbxfFRppWxO5bEXOrKdryg/DUPpQ9xXpKPsGKikeAMzrSHe diff --git a/pkg/resources/config/rbac/kustomization.yaml b/pkg/resources/resources/registry/service.yaml similarity index 74% copy from pkg/resources/config/rbac/kustomization.yaml copy to pkg/resources/resources/registry/service.yaml index d6a983f2c..a28da8131 100644 --- a/pkg/resources/config/rbac/kustomization.yaml +++ b/pkg/resources/resources/registry/service.yaml @@ -15,16 +15,19 @@ # limitations under the License. # --------------------------------------------------------------------------- -# -# rbac resources applicable for all kubernetes platforms -# -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: -- builder-role.yaml -- builder-role-openshift.yaml -- kamelets-viewer-role.yaml -- builder-role-binding.yaml -- builder-role-binding-openshift.yaml -- kamelets-viewer-role-binding.yaml +apiVersion: v1 +kind: Service +metadata: + name: registry +spec: + selector: + app: registry + ports: + - name: http + port: 80 + protocol: TCP + targetPort: 5000 + - name: https + port: 443 + protocol: TCP + targetPort: 443 diff --git a/pkg/util/kubernetes/docker_secret.go b/pkg/util/kubernetes/docker_secret.go new file mode 100644 index 000000000..56b48ff18 --- /dev/null +++ b/pkg/util/kubernetes/docker_secret.go @@ -0,0 +1,110 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubernetes + +import ( + "context" + "crypto/tls" + "encoding/base64" + "encoding/json" + "fmt" + "os" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type dockerConfigJSON struct { + Auths map[string]dockerConfigEntry `json:"auths"` +} + +type dockerConfigEntry struct { + Username string `json:"username"` + Password string `json:"password"` + Auth string `json:"auth"` +} + +func buildDockerConfigJSON(server, username, password string) ([]byte, error) { + auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) + cfg := dockerConfigJSON{ + Auths: map[string]dockerConfigEntry{ + server: { + Username: username, + Password: password, + Auth: auth, + }, + }, + } + return json.Marshal(cfg) +} + +// DockerRegistrySecret returns a kubernetes.io/dockerconfigjson secret. +func DockerRegistrySecret(ctx context.Context, namespace, name, server, username, password string) (*corev1.Secret, error) { + dockerCfgJSON, err := buildDockerConfigJSON(server, username, password) + if err != nil { + return nil, fmt.Errorf("building docker config json: %w", err) + } + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: dockerCfgJSON, + }, + } + + return secret, nil +} + +// TLSSecretFromFiles mirrors `kubectl create secret tls --cert=<path> --key=<path>`, +// reading the cert/key from disk and validating them the same way kubectl does +// (tls.X509KeyPair) before submitting to the API. +func TLSSecretFromFiles(ctx context.Context, namespace, name string, certPath, keyPath string) (*corev1.Secret, error) { + certData, err := os.ReadFile(certPath) + if err != nil { + return nil, fmt.Errorf("reading cert file %q: %w", certPath, err) + } + + keyData, err := os.ReadFile(keyPath) + if err != nil { + return nil, fmt.Errorf("reading key file %q: %w", keyPath, err) + } + + // Same validation kubectl performs: parses PEM blocks and confirms the + // private key matches the certificate's public key. + if _, err := tls.X509KeyPair(certData, keyData); err != nil { + return nil, fmt.Errorf("failed to load key pair: %w", err) + } + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + corev1.TLSCertKey: certData, + corev1.TLSPrivateKeyKey: keyData, + }, + } + + return secret, nil +}
