Skip to content

Commit

Permalink
Initial implementation of refresher command
Browse files Browse the repository at this point in the history
This command allows to have a repilcaset that only manages a single
secret. Useful when there's no need for a full fledged solution with
dynamic detection of namespaces.
  • Loading branch information
turip committed Apr 7, 2021
1 parent e5fc942 commit a6f7c09
Show file tree
Hide file tree
Showing 6 changed files with 344 additions and 3 deletions.
32 changes: 32 additions & 0 deletions Dockerfile-refresher
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright (c) 2019 Banzai Cloud Zrt. All Rights Reserved.

# Build the manager binary
FROM golang:1.15 as builder

ARG GOPROXY

ENV GOFLAGS="-mod=readonly"

WORKDIR /workspace/
# Copy the Go Modules manifests

COPY ./go.mod /workspace/
COPY ./go.sum /workspace/
RUN go mod download

COPY ./ /workspace/

# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager ./cmd/controller/

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM alpine:3.13.4

RUN apk add --update --no-cache ca-certificates

WORKDIR /
COPY --from=builder /workspace/manager .
USER nonroot:nonroot

ENTRYPOINT ["/manager"]
14 changes: 12 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,27 @@ lint-fix: bin/golangci-lint ## Run linter & fix
bin/golangci-lint run -c .golangci.yml --fix

.PHONY: build
build: generate fmt vet ## Build the binary
go build ${GOARGS} -o bin/${SERVICE_NAME} -ldflags "${LDFLAGS}" ${MAIN_PACKAGE}
build: generate fmt vet binary ## Build the binary

.PHONY: build-refresher
build: generate fmt vet binary-refresher ## Build the binary

.PHONY: binary
binary: ## Build the binary without executing any code generators
go build ${GOARGS} -o bin/${SERVICE_NAME} -ldflags "${LDFLAGS}" ${MAIN_PACKAGE}

.PHONY: binary-refresher
binary-refresher: ## Build the refresher binary without executing any code generators
go build ${GOARGS} -o bin/${SERVICE_NAME}-refresher -ldflags "${LDFLAGS}" ./cmd/refresher

.PHONY: run
run: generate fmt vet manifests ## Run against the configured Kubernetes cluster in ~/.kube/config
go run ${GOARGS} ${MAIN_PACKAGE}

.PHONY: static
static:


.PHONY: ensure-tools
ensure-tools:
@scripts/download-deps.sh
Expand Down
2 changes: 2 additions & 0 deletions main.go → cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ func main() {
os.Exit(1)
}

pflag.Parse()

periodicReconcileIntervalDuration := time.Duration(periodicReconcileInterval) * time.Second

// Create logger (first thing after configuration loading)
Expand Down
154 changes: 154 additions & 0 deletions cmd/refresher/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package main

import (
"fmt"
"os"
"strings"
"time"

"emperror.dev/errors"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
logrintegration "logur.dev/integration/logr"
"logur.dev/logur"
ctrl "sigs.k8s.io/controller-runtime"

"github.com/banzaicloud/imps/controllers"
"github.com/banzaicloud/imps/internal/errorhandler"
"github.com/banzaicloud/imps/internal/log"
"github.com/banzaicloud/imps/pkg/ecr"
"github.com/banzaicloud/operator-tools/pkg/reconciler"
)

var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
ErrInvalidReference = errors.New("invalid resource reference name")
ErrNoSourceSecrets = errors.New("no source secrets are specified")
)

func init() {
_ = clientgoscheme.AddToScheme(scheme)

// +kubebuilder:scaffold:scheme
}

type Config struct {
Log log.Config
}

func refToNamespacedName(name string) (*types.NamespacedName, error) {
parts := strings.Split(name, ".")
if len(parts) <= 1 || len(parts) > 2 {
return nil, errors.WrapWithDetails(ErrInvalidReference, "reference", name)
}

return &types.NamespacedName{
Namespace: parts[0],
Name: parts[1],
}, nil
}

func main() {
Configure(viper.GetViper(), pflag.CommandLine)
var periodicReconcileInterval int
var targetSecretString string

sourceSecretStrings := pflag.StringArray("source-secret", nil, "Source secrets specified in <namespace>.<secret-name> format")
pflag.IntVar(&periodicReconcileInterval, "periodic-reconcile-interval", 300, "The interval in seconds in which controller reconciles are run periodically.")
pflag.StringVar(&targetSecretString, "target-secret", "", "Target secret specifies what secret to create containing the image pull secrets. Format: namespace.secret-name")
pflag.Parse()

var config Config
err := viper.Unmarshal(&config)
if err != nil {
setupLog.Error(err, "failed to unmarshal configuration")
os.Exit(1)
}

// Parse command line arguments
targetSecret, err := refToNamespacedName(targetSecretString)
if err != nil {
setupLog.Error(err, "failed to parse target secret name")
os.Exit(1)
}

if len(*sourceSecretStrings) == 0 {
setupLog.Error(ErrNoSourceSecrets, "please specify source secrets")
os.Exit(1)
}

sourceSecrets := []types.NamespacedName{}
for _, sourceSecertString := range *sourceSecretStrings {
sourceSecret, err := refToNamespacedName(sourceSecertString)
if err != nil {
setupLog.Error(err, "failed to parse source secret name")
os.Exit(1)
}
sourceSecrets = append(sourceSecrets, *sourceSecret)
}

// Create logger (first thing after configuration loading)
logger := log.NewLogger(config.Log)
ctrl.SetLogger(logrintegration.New(logger))

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}

ecrLogger := logur.WithField(logger, "controller", "ecr_token_refresh")
ecr.Initialize(ecrLogger)

errorHandler := errorhandler.New(logger)

periodicReconcileIntervalDuration := time.Duration(periodicReconcileInterval) * time.Second

refresherLogger := logur.WithField(logger, "controller", "imagepullsecrets")
refresherReconciler := &controllers.RefresherReconciler{
Client: mgr.GetClient(),
Log: refresherLogger,
ErrorHandler: errorHandler,
Scheme: mgr.GetScheme(),
ResourceReconciler: reconciler.NewReconcilerWith(mgr.GetClient(), reconciler.WithLog(logrintegration.New(refresherLogger))),
PeriodicReconcileInterval: periodicReconcileIntervalDuration,
SourceSecrets: sourceSecrets,
TargetSecret: *targetSecret,
}

if err = refresherReconciler.SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "imagepullsecrets")
os.Exit(1)
}

setupLog.Info("starting manager")

if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}

const FriendlyServiceName = "imps"

func Configure(v *viper.Viper, p *pflag.FlagSet) {
v.AllowEmptyEnv(true)
p.Init(FriendlyServiceName, pflag.ExitOnError)
pflag.Usage = func() {
_, _ = fmt.Fprintf(os.Stderr, "Usage of %s:\n", FriendlyServiceName)
pflag.PrintDefaults()
}

v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
v.AutomaticEnv()

log.ConfigureLoggingFlags(v, p)

_ = v.BindPFlags(p)
}
143 changes: 143 additions & 0 deletions controllers/refresher_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright © 2021 Banzai Cloud
//
// 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"
"time"

"emperror.dev/errors"
ctrlBuilder "sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/source"

"github.com/banzaicloud/imps/pkg/pullsecrets"

"emperror.dev/emperror"
"logur.dev/logur"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/banzaicloud/imps/internal/cron"
"github.com/banzaicloud/operator-tools/pkg/reconciler"
)

// RefresherReconciler reconciles a AlertingPolicy object
type RefresherReconciler struct {
client.Client
Log logur.Logger
ErrorHandler emperror.ErrorHandler
Scheme *runtime.Scheme

ResourceReconciler reconciler.ResourceReconciler
PeriodicReconcileInterval time.Duration
SourceSecrets []types.NamespacedName
TargetSecret types.NamespacedName
}

// +kubebuilder:rbac:groups=images.banzaicloud.io,resources=imagepullsecrets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=images.banzaicloud.io,resources=imagepullsecrets/status,verbs=get;update;patch
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=events,verbs=create;update;patch
func (r *RefresherReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
result, err := r.reconcile(req)
result, err = cron.EnsurePeriodicReconcile(r.PeriodicReconcileInterval, result, err)
if err != nil {
r.ErrorHandler.Handle(err)
}
return result, err
}

func (r *RefresherReconciler) SetupWithManager(mgr ctrl.Manager) error {
builder := ctrl.NewControllerManagedBy(mgr).
For(&corev1.Secret{}, ctrlBuilder.WithPredicates(predicate.GenerationChangedPredicate{})).
Watches(
&source.Kind{Type: &corev1.Secret{}},
&handler.EnqueueRequestsFromMapFunc{
ToRequests: handler.ToRequestsFunc(r.isMatchingSecret),
})

return builder.Complete(r)
}

func (r *RefresherReconciler) isMatchingSecret(obj handler.MapObject) []ctrl.Request {
secret, ok := obj.Object.(*corev1.Secret)
if !ok {
r.Log.Info("object is not a Secret")
return []ctrl.Request{}
}

reconcileTargetRequest := ctrl.Request{
NamespacedName: r.TargetSecret,
}

secretRef := types.NamespacedName{Namespace: secret.Namespace, Name: secret.Name}

if r.isSourceSecret(secretRef) || r.isTargetSecret(secretRef) {
return []ctrl.Request{reconcileTargetRequest}
}

return []ctrl.Request{}
}

func (r *RefresherReconciler) isSourceSecret(secret types.NamespacedName) bool {
for _, sourceSecret := range r.SourceSecrets {
if sourceSecret.Namespace == secret.Namespace && sourceSecret.Name == secret.Name {
return true
}
}

return false
}

func (r *RefresherReconciler) isTargetSecret(secret types.NamespacedName) bool {
return r.TargetSecret.Namespace == secret.Namespace && r.TargetSecret.Name == secret.Name
}

func (r *RefresherReconciler) reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
logger := logur.WithField(r.Log, "imagepullsecret", req.NamespacedName)
result := ctrl.Result{}

if !r.isSourceSecret(req.NamespacedName) && !r.isTargetSecret(req.NamespacedName) {
return result, nil
}

config, err := pullsecrets.NewConfigFromSecrets(ctx, r, r.SourceSecrets)
if err != nil {
return result, errors.WithStack(err)
}

pullSecret, pullSecretExpires, err := config.Secret(ctx, r.TargetSecret.Namespace, r.TargetSecret.Name)
if err != nil {
return result, errors.WrapWithDetails(err, "cannot get referenced secret")
}

_, err = r.ResourceReconciler.ReconcileResource(pullSecret, reconciler.StatePresent)
if err != nil {
return result, err
}

logger.Info("successfully reconciled secret", map[string]interface{}{
"expiration": pullSecretExpires,
})

return result, nil
}
2 changes: 1 addition & 1 deletion pkg/ecr/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func (t *TokenManager) GetAuthorizationToken(ctx context.Context, key Stringable
return ecr_types.AuthorizationData{}, err
}
t.ManagedTokens[key.String()] = token
if token.CurrentToken != nil {
if token.CurrentToken == nil {
return ecr_types.AuthorizationData{}, errors.New("no token is available")
}
t.Logger.Info("token refreshed", map[string]interface{}{
Expand Down

0 comments on commit a6f7c09

Please sign in to comment.