Skip to content

Fix namespace setting with direct applier #173

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 62 additions & 11 deletions pkg/patterns/declarative/pkg/applier/direct.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,71 @@ package applier

import (
"context"
"fmt"
"os"
"strings"

"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/discovery"
"k8s.io/client-go/rest"
"k8s.io/kubectl/pkg/cmd/apply"
cmdDelete "k8s.io/kubectl/pkg/cmd/delete"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)

type DirectApplier struct {
a apply.ApplyOptions
}

var _ Applier = &DirectApplier{}

func NewDirectApplier() *DirectApplier {
return &DirectApplier{}
}

func (d *DirectApplier) Apply(ctx context.Context,
namespace string,
manifest string,
validate bool,
extraArgs ...string,
) error {
func (d *DirectApplier) Apply(ctx context.Context, opt ApplierOptions) error {
ioStreams := genericclioptions.IOStreams{
In: os.Stdin,
Out: os.Stdout,
ErrOut: os.Stderr,
}
restClient := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag()
ioReader := strings.NewReader(manifest)
ioReader := strings.NewReader(opt.Manifest)

restClientGetter := &staticRESTClientGetter{
RESTMapper: opt.RESTMapper,
RESTConfig: opt.RESTConfig,
}
b := resource.NewBuilder(restClientGetter)

if opt.Validate {
// This potentially causes redundant work, but validation isn't the common path
v, err := cmdutil.NewFactory(&genericclioptions.ConfigFlags{}).Validator(true)
if err != nil {
return err
}
b.Schema(v)
}

b := resource.NewBuilder(restClient)
res := b.Unstructured().Stream(ioReader, "manifestString").Do()
infos, err := res.Infos()
if err != nil {
return err
}

// Populate the namespace on any namespace-scoped objects
if opt.Namespace != "" {
visitor := resource.SetNamespace(opt.Namespace)
for _, info := range infos {
if err := info.Visit(visitor); err != nil {
return fmt.Errorf("error from SetNamespace: %w", err)
}
}
}

applyOpts := apply.NewApplyOptions(ioStreams)
applyOpts.Namespace = namespace
applyOpts.Namespace = opt.Namespace
applyOpts.SetObjects(infos)
applyOpts.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
applyOpts.PrintFlags.NamePrintFlags.Operation = operation
Expand All @@ -56,3 +79,31 @@ func (d *DirectApplier) Apply(ctx context.Context,

return applyOpts.Run()
}

// staticRESTClientGetter returns a fixed RESTClient
type staticRESTClientGetter struct {
RESTConfig *rest.Config
DiscoveryClient discovery.CachedDiscoveryInterface
RESTMapper meta.RESTMapper
}

var _ resource.RESTClientGetter = &staticRESTClientGetter{}

func (s *staticRESTClientGetter) ToRESTConfig() (*rest.Config, error) {
if s.RESTConfig == nil {
return nil, fmt.Errorf("RESTConfig not set")
}
return s.RESTConfig, nil
}
func (s *staticRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
if s.DiscoveryClient == nil {
return nil, fmt.Errorf("DiscoveryClient not set")
}
return s.DiscoveryClient, nil
}
func (s *staticRESTClientGetter) ToRESTMapper() (meta.RESTMapper, error) {
if s.RESTMapper == nil {
return nil, fmt.Errorf("RESTMapper not set")
}
return s.RESTMapper, nil
}
17 changes: 9 additions & 8 deletions pkg/patterns/declarative/pkg/applier/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type ExecKubectl struct {
cmdSite commandSite
}

var _ Applier = &ExecKubectl{}

// commandSite allows for tests to mock cmd.Run() events
type commandSite interface {
Run(*exec.Cmd) error
Expand All @@ -49,26 +51,25 @@ func (console) Run(c *exec.Cmd) error {
}

// Apply runs the kubectl apply with the provided manifest argument
func (c *ExecKubectl) Apply(ctx context.Context, namespace string, manifest string, validate bool,
extraArgs ...string) error {
func (c *ExecKubectl) Apply(ctx context.Context, opt ApplierOptions) error {
log := log.Log

log.Info("applying manifest")

args := []string{"apply"}
if namespace != "" {
args = append(args, "-n", namespace)
if opt.Namespace != "" {
args = append(args, "-n", opt.Namespace)
}

// Not doing --validate avoids downloading the OpenAPI
// which can save a lot work & memory
args = append(args, "--validate="+strconv.FormatBool(validate))
args = append(args, "--validate="+strconv.FormatBool(opt.Validate))

args = append(args, extraArgs...)
args = append(args, opt.ExtraArgs...)
args = append(args, "-f", "-")

cmd := exec.Command("kubectl", args...)
cmd.Stdin = strings.NewReader(manifest)
cmd.Stdin = strings.NewReader(opt.Manifest)

var stdout bytes.Buffer
var stderr bytes.Buffer
Expand All @@ -80,7 +81,7 @@ func (c *ExecKubectl) Apply(ctx context.Context, namespace string, manifest stri
err := c.cmdSite.Run(cmd)
if err != nil {
log.WithValues("stdout", stdout.String()).WithValues("stderr", stderr.String()).Error(err, "error from running kubectl apply")
log.Info(fmt.Sprintf("manifest:\n%v", manifest))
log.Info(fmt.Sprintf("manifest:\n%v", opt.Manifest))
return fmt.Errorf("error from running kubectl apply: %v", err)
}

Expand Down
8 changes: 7 additions & 1 deletion pkg/patterns/declarative/pkg/applier/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,13 @@ func TestKubectlApply(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
cs := collector{Error: test.err}
kubectl := &ExecKubectl{cmdSite: &cs}
err := kubectl.Apply(context.Background(), test.namespace, test.manifest, test.validate, test.args...)
opts := ApplierOptions{
Namespace: test.namespace,
Manifest: test.manifest,
Validate: test.validate,
ExtraArgs: test.args,
}
err := kubectl.Apply(context.Background(), opts)

if test.err != nil && err == nil {
t.Error("expected error to occur")
Expand Down
15 changes: 14 additions & 1 deletion pkg/patterns/declarative/pkg/applier/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,21 @@ package applier

import (
"context"

"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/client-go/rest"
)

type Applier interface {
Apply(ctx context.Context, namespace string, manifest string, validate bool, extraArgs ...string) error
Apply(ctx context.Context, options ApplierOptions) error
}

type ApplierOptions struct {
Manifest string

RESTConfig *rest.Config
RESTMapper meta.RESTMapper
Namespace string
Validate bool
ExtraArgs []string
}
16 changes: 12 additions & 4 deletions pkg/patterns/declarative/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,16 @@ func (r *Reconciler) reconcileExists(ctx context.Context, name types.NamespacedN
}
}

if err := r.kubectl.Apply(ctx, ns, manifestStr, r.options.validate, extraArgs...); err != nil {
applyOpt := applier.ApplierOptions{
RESTConfig: r.config,
RESTMapper: r.restMapper,
Namespace: ns,
Manifest: manifestStr,
Validate: r.options.validate,
ExtraArgs: extraArgs,
}

if err := r.kubectl.Apply(ctx, applyOpt); err != nil {
log.Error(err, "applying manifest")
return reconcile.Result{}, fmt.Errorf("error applying manifest: %v", err)
}
Expand Down Expand Up @@ -564,14 +573,13 @@ func (r *Reconciler) CollectMetrics() bool {
return r.options.metrics
}

func GetObjectFromCluster(obj *manifest.Object, r *Reconciler) (*unstructured.
Unstructured, error) {
func GetObjectFromCluster(obj *manifest.Object, r *Reconciler) (*unstructured.Unstructured, error) {
getOptions := metav1.GetOptions{}
gvk := obj.GroupVersionKind()

mapping, err := r.restMapper.RESTMapping(obj.GroupKind(), gvk.Version)
if err != nil {
return nil, fmt.Errorf("unable to get mapping for resource: %w", err)
return nil, fmt.Errorf("unable to get mapping for resource %v: %w", gvk, err)
}
ns := obj.UnstructuredObject().GetNamespace()
unstruct, err := r.dynamicClient.Resource(mapping.Resource).Namespace(ns).Get(context.Background(),
Expand Down