Skip to content

Commit

Permalink
Merge pull request #538 from fluxcd/image-controller-commands
Browse files Browse the repository at this point in the history
Add commands for image automation API
  • Loading branch information
squaremo authored Dec 11, 2020
2 parents 16f5261 + 0e35c20 commit 0ba6fc1
Show file tree
Hide file tree
Showing 70 changed files with 3,317 additions and 11 deletions.
80 changes: 79 additions & 1 deletion cmd/flux/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,19 @@ limitations under the License.
package main

import (
"context"
"fmt"
"strings"
"time"

"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

"github.com/spf13/cobra"
"github.com/fluxcd/flux2/internal/utils"
)

var createCmd = &cobra.Command{
Expand All @@ -46,6 +52,78 @@ func init() {
rootCmd.AddCommand(createCmd)
}

// upsertable is an interface for values that can be used in `upsert`.
type upsertable interface {
adapter
named
}

// upsert updates or inserts an object. Instead of providing the
// object itself, you provide a named (as in Name and Namespace)
// template value, and a mutate function which sets the values you
// want to update. The mutate function is nullary -- you mutate a
// value in the closure, e.g., by doing this:
//
// var existing Value
// existing.Name = name
// existing.Namespace = ns
// upsert(ctx, client, valueAdapter{&value}, func() error {
// value.Spec = onePreparedEarlier
// })
func (names apiType) upsert(ctx context.Context, kubeClient client.Client, object upsertable, mutate func() error) (types.NamespacedName, error) {
nsname := types.NamespacedName{
Namespace: object.GetNamespace(),
Name: object.GetName(),
}

op, err := controllerutil.CreateOrUpdate(ctx, kubeClient, object.asRuntimeObject(), mutate)
if err != nil {
return nsname, err
}

switch op {
case controllerutil.OperationResultCreated:
logger.Successf("%s created", names.kind)
case controllerutil.OperationResultUpdated:
logger.Successf("%s updated", names.kind)
}
return nsname, nil
}

type upsertWaitable interface {
upsertable
statusable
}

// upsertAndWait encodes the pattern of creating or updating a
// resource, then waiting for it to reconcile. See the note on
// `upsert` for how to work with the `mutate` argument.
func (names apiType) upsertAndWait(object upsertWaitable, mutate func() error) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

kubeClient, err := utils.KubeClient(kubeconfig, kubecontext) // NB globals
if err != nil {
return err
}

logger.Generatef("generating %s", names.kind)
logger.Actionf("applying %s", names.kind)

namespacedName, err := imageRepositoryType.upsert(ctx, kubeClient, object, mutate)
if err != nil {
return err
}

logger.Waitingf("waiting for %s reconciliation", names.kind)
if err := wait.PollImmediate(pollInterval, timeout,
isReady(ctx, kubeClient, namespacedName, object)); err != nil {
return err
}
logger.Successf("%s reconciliation completed", names.kind)
return nil
}

func parseLabels() (map[string]string, error) {
result := make(map[string]string)
for _, label := range labels {
Expand Down
38 changes: 38 additions & 0 deletions cmd/flux/create_image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
Copyright 2020 The Flux authors
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 main

import (
"strings"

"github.com/spf13/cobra"
)

const createImageLong = `
The create image sub-commands work with image automation objects; that is,
object controlling updates to git based on e.g., new container images
being available.`

var createImageCmd = &cobra.Command{
Use: "image",
Short: "Create or update resources dealing with image automation",
Long: strings.TrimSpace(createImageLong),
}

func init() {
createCmd.AddCommand(createImageCmd)
}
110 changes: 110 additions & 0 deletions cmd/flux/create_image_policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
Copyright 2020 The Flux authors
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 main

import (
"fmt"

"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

imagev1 "github.com/fluxcd/image-reflector-controller/api/v1alpha1"
)

var createImagePolicyCmd = &cobra.Command{
Use: "policy <name>",
Short: "Create or update an ImagePolicy object",
Long: `The create image policy command generates an ImagePolicy resource.
An ImagePolicy object calculates a "latest image" given an image
repository and a policy, e.g., semver.
The image that sorts highest according to the policy is recorded in
the status of the object.`,
RunE: createImagePolicyRun}

type imagePolicyFlags struct {
imageRef string
semver string
}

var imagePolicyArgs = imagePolicyFlags{}

func init() {
flags := createImagePolicyCmd.Flags()
flags.StringVar(&imagePolicyArgs.imageRef, "image-ref", "", "the name of an image repository object")
flags.StringVar(&imagePolicyArgs.semver, "semver", "", "a semver range to apply to tags; e.g., '1.x'")

createImageCmd.AddCommand(createImagePolicyCmd)
}

// getObservedGeneration is implemented here, since it's not
// (presently) needed elsewhere.
func (obj imagePolicyAdapter) getObservedGeneration() int64 {
return obj.ImagePolicy.Status.ObservedGeneration
}

func createImagePolicyRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("ImagePolicy name is required")
}
objectName := args[0]

if imagePolicyArgs.imageRef == "" {
return fmt.Errorf("the name of an ImageRepository in the namespace is required (--image-ref)")
}

labels, err := parseLabels()
if err != nil {
return err
}

var policy = imagev1.ImagePolicy{
ObjectMeta: metav1.ObjectMeta{
Name: objectName,
Namespace: namespace,
Labels: labels,
},
Spec: imagev1.ImagePolicySpec{
ImageRepositoryRef: corev1.LocalObjectReference{
Name: imagePolicyArgs.imageRef,
},
},
}

switch {
case imagePolicyArgs.semver != "":
policy.Spec.Policy.SemVer = &imagev1.SemVerPolicy{
Range: imagePolicyArgs.semver,
}
default:
return fmt.Errorf("a policy must be provided with --semver")
}

if export {
return printExport(exportImagePolicy(&policy))
}

var existing imagev1.ImagePolicy
copyName(&existing, &policy)
err = imagePolicyType.upsertAndWait(imagePolicyAdapter{&existing}, func() error {
existing.Spec = policy.Spec
existing.SetLabels(policy.Labels)
return nil
})
return err
}
110 changes: 110 additions & 0 deletions cmd/flux/create_image_repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
Copyright 2020 The Flux authors
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 main

import (
"fmt"
"time"

"github.com/google/go-containerregistry/pkg/name"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

imagev1 "github.com/fluxcd/image-reflector-controller/api/v1alpha1"
)

var createImageRepositoryCmd = &cobra.Command{
Use: "repository <name>",
Short: "Create or update an ImageRepository object",
Long: `The create image repository command generates an ImageRepository resource.
An ImageRepository object specifies an image repository to scan.`,
RunE: createImageRepositoryRun,
}

type imageRepoFlags struct {
image string
secretRef string
timeout time.Duration
}

var imageRepoArgs = imageRepoFlags{}

func init() {
flags := createImageRepositoryCmd.Flags()
flags.StringVar(&imageRepoArgs.image, "image", "", "the image repository to scan; e.g., library/alpine")
flags.StringVar(&imageRepoArgs.secretRef, "secret-ref", "", "the name of a docker-registry secret to use for credentials")
// NB there is already a --timeout in the global flags, for
// controlling timeout on operations while e.g., creating objects.
flags.DurationVar(&imageRepoArgs.timeout, "scan-timeout", 0, "a timeout for scanning; this defaults to the interval if not set")

createImageCmd.AddCommand(createImageRepositoryCmd)
}

func createImageRepositoryRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("ImageRepository name is required")
}
objectName := args[0]

if imageRepoArgs.image == "" {
return fmt.Errorf("an image repository (--image) is required")
}

if _, err := name.NewRepository(imageRepoArgs.image); err != nil {
return fmt.Errorf("unable to parse image value: %w", err)
}

labels, err := parseLabels()
if err != nil {
return err
}

var repo = imagev1.ImageRepository{
ObjectMeta: metav1.ObjectMeta{
Name: objectName,
Namespace: namespace,
Labels: labels,
},
Spec: imagev1.ImageRepositorySpec{
Image: imageRepoArgs.image,
Interval: metav1.Duration{Duration: interval},
},
}
if imageRepoArgs.timeout != 0 {
repo.Spec.Timeout = &metav1.Duration{Duration: imageRepoArgs.timeout}
}
if imageRepoArgs.secretRef != "" {
repo.Spec.SecretRef = &corev1.LocalObjectReference{
Name: imageRepoArgs.secretRef,
}
}

if export {
return printExport(exportImageRepository(&repo))
}

// a temp value for use with the rest
var existing imagev1.ImageRepository
copyName(&existing, &repo)
err = imageRepositoryType.upsertAndWait(imageRepositoryAdapter{&existing}, func() error {
existing.Spec = repo.Spec
existing.Labels = repo.Labels
return nil
})
return err
}
Loading

0 comments on commit 0ba6fc1

Please sign in to comment.