Skip to content

Commit

Permalink
✨ Add SecretSyncer controller to save pull secret data locally
Browse files Browse the repository at this point in the history
  • Loading branch information
anik120 committed Oct 1, 2024
1 parent f169414 commit 7e1b8f7
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 1 deletion.
33 changes: 32 additions & 1 deletion cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"time"

"github.com/containers/image/v5/types"
Expand Down Expand Up @@ -68,7 +69,7 @@ var (
defaultSystemNamespace = "olmv1-system"
)

const authFilePath = "/etc/operator-controller/auth.json"
const authFilePath = "/tmp/operator-controller/auth.json"

// podNamespace checks whether the controller is running in a Pod vs.
// being run locally by inspecting the namespace file that gets mounted
Expand All @@ -91,6 +92,7 @@ func main() {
operatorControllerVersion bool
systemNamespace string
caCertDir string
globalPullSecret string
)
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
Expand All @@ -101,6 +103,7 @@ func main() {
flag.StringVar(&cachePath, "cache-path", "/var/cache", "The local directory path used for filesystem based caching")
flag.BoolVar(&operatorControllerVersion, "version", false, "Prints operator-controller version information")
flag.StringVar(&systemNamespace, "system-namespace", "", "Configures the namespace that gets used to deploy system resources.")
flag.StringVar(&globalPullSecret, "global-pull-secret", "", "The namespace/name of the global pull secret that is going to be used to pull bundle images.")

klog.InitFlags(flag.CommandLine)

Expand All @@ -117,6 +120,19 @@ func main() {

setupLog.Info("starting up the controller", "version info", version.String())

if globalPullSecret != "" {
dir := filepath.Dir(authFilePath)
if err := os.MkdirAll(dir, 0755); err != nil {
setupLog.Error(err, "unable to create directory for storing auth")
os.Exit(1)
}
if f, err := os.OpenFile(authFilePath, os.O_CREATE|os.O_WRONLY, 0644); err != nil {
setupLog.Error(err, "failed to create file.")
} else {
f.Close()
}
}

if systemNamespace == "" {
systemNamespace = podNamespace()
}
Expand Down Expand Up @@ -289,6 +305,21 @@ func main() {
os.Exit(1)
}

if globalPullSecret != "" {
secretParts := strings.Split(globalPullSecret, "/")
setupLog.Info("Creating SecretSyncer controller for watching secret", secretParts[1], "in namespace", secretParts[0])
err := (&controllers.SecretSyncerReconciler{
Client: mgr.GetClient(),
AuthFilePath: authFilePath,
SecretName: secretParts[1],
SecretNamespace: secretParts[0],
}).SetupWithManager(mgr)
if err != nil {
setupLog.Error(err, "unable to create controller", "controller", "SecretSyncer")
os.Exit(1)
}
}

//+kubebuilder:scaffold:builder

if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
Expand Down
124 changes: 124 additions & 0 deletions internal/controllers/secret_syncer_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
Copyright 2024.
Licensed 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 controllers

import (
"context"
"encoding/json"
"fmt"
"os"

"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

// SecretSyncerReconciler reconciles a specific secret object
type SecretSyncerReconciler struct {
client.Client
SecretName string
SecretNamespace string
AuthFilePath string
}

func (r *SecretSyncerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)

if req.Name != r.SecretName || req.Namespace != r.SecretNamespace {
logger.Info("Received unexpected request for secret", req.Name, "in namespace", req.Namespace)
return ctrl.Result{}, nil
}

secret := &corev1.Secret{}
err := r.Get(ctx, req.NamespacedName, secret)
if err != nil {
if apierrors.IsNotFound(err) {
logger.Info("Secret", req.Name, "in namespace", req.Namespace, "deleted.")
return r.emptySecretFile(logger)
}
logger.Error(err, "Failed to get Secret")
return ctrl.Result{}, err
}

return r.writeSecretToFile(secret, logger)
}

// SetupWithManager sets up the controller with the Manager.
func (r *SecretSyncerReconciler) SetupWithManager(mgr ctrl.Manager) error {
_, err := ctrl.NewControllerManagedBy(mgr).
For(&corev1.Secret{}).
WithEventFilter(newSecretPredicate(r.SecretName, r.SecretNamespace)).
Build(r)

return err
}

func newSecretPredicate(secretName, secretNamespace string) predicate.Predicate {
return predicate.NewPredicateFuncs(func(obj client.Object) bool {
secret, ok := obj.(*corev1.Secret)
if !ok {
return false
}
return secret.Name == secretName && secret.Namespace == secretNamespace
})
}

// writeSecretToFile writes the secret data to the specified file
func (r *SecretSyncerReconciler) writeSecretToFile(secret *corev1.Secret, logger logr.Logger) (ctrl.Result, error) {
secretData := make(map[string]string)
for key, value := range secret.Data {
// image registry secrets are always stored with the key .dockerconfigjson
// ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#registry-secret-existing-credentials
if key == ".dockerconfigjson" {
secretData[key] = string(value)
}
}

jsonData, err := json.Marshal(secretData)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to marshal secret data: %w", err)
}
err = os.WriteFile(r.AuthFilePath, jsonData, 0644)

Check failure on line 99 in internal/controllers/secret_syncer_controller.go

View workflow job for this annotation

GitHub Actions / lint

G306: Expect WriteFile permissions to be 0600 or less (gosec)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to write secret data to file: %w", err)
}
logger.Info("Saved Secret data locally")
return ctrl.Result{}, nil
}

// emptySecretFile deletes the secret data from the file if the secret is deleted
func (r *SecretSyncerReconciler) emptySecretFile(logger logr.Logger) (ctrl.Result, error) {
logger.Info("Emptying local auth file.")
if _, err := os.Stat(r.AuthFilePath); err == nil {
file, err := os.OpenFile(r.AuthFilePath, os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to open and empty secret file: %w", err)
}
defer file.Close()
logger.Info("Auth file emptied successfully", "file", r.AuthFilePath)
} else if os.IsNotExist(err) {
logger.Info("Secret file does not exist, nothing to empty", "file", r.AuthFilePath)
} else {
return ctrl.Result{}, fmt.Errorf("failed to check secret file: %w", err)
}

return ctrl.Result{}, nil
}

0 comments on commit 7e1b8f7

Please sign in to comment.