Skip to content

Commit

Permalink
Merge pull request #1891 from maqiuyujoyce/202405-llm-powertool
Browse files Browse the repository at this point in the history
Powertool to update state-into-spec value
  • Loading branch information
google-oss-prow[bot] authored May 29, 2024
2 parents a4c2af8 + 12a1e42 commit bbdd7e2
Show file tree
Hide file tree
Showing 26 changed files with 1,920 additions and 82 deletions.
9 changes: 9 additions & 0 deletions config/tests/samples/create/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -558,11 +558,20 @@ func (h *Harness) GetClient() client.Client {
return h.client
}

func (h *Harness) GetRESTConfig() *rest.Config {
return h.restConfig
}

func MaybeSkip(t *testing.T, name string, resources []*unstructured.Unstructured) {
if os.Getenv("E2E_GCP_TARGET") == "mock" {
for _, resource := range resources {
gvk := resource.GroupVersionKind()

// Special fake types for testing
if gvk.Group == "" && gvk.Kind == "RunCLI" {
continue
}

switch gvk.Group {
case "core.cnrm.cloud.google.com":
continue
Expand Down
37 changes: 30 additions & 7 deletions pkg/cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package cmd

import (
"bytes"
"fmt"
"io/ioutil"
golog "log"
Expand Down Expand Up @@ -89,23 +90,45 @@ func recoverExecute() (err error) {
}

func execute() error {
defaultToBulkExport()
defaultToBulkExport(os.Args)
return rootCmd.Execute()
}

func defaultToBulkExport() {
type TestInvocationOptions struct {
Stdout bytes.Buffer
Stderr bytes.Buffer
Stdin bytes.Buffer
Args []string
}

// ExecuteFromTest allows for invocation of the CLI from a test
func ExecuteFromTest(options *TestInvocationOptions) error {
rootCmd.SetIn(&options.Stdin)
rootCmd.SetOut(&options.Stdout)
rootCmd.SetErr(&options.Stderr)
rootCmd.SetArgs(options.Args[1:])

defaultToBulkExport(options.Args)
err := rootCmd.Execute()
if err != nil {
fmt.Fprintf(&options.Stderr, "%v\n", err)
}
return err
}

func defaultToBulkExport(args []string) {
// previously this command had no sub-commands and effectively defaulted to the bulk-export command for backwards
// compatibility, if there is no sub-command and the flags appear to be the legacy flags format, default to the
// bulk-export sub-command
if isLegacyArgs() {
newArgs := sanitizeArgsForBackwardsCompatibility(os.Args[1:])
if isLegacyArgs(args) {
newArgs := sanitizeArgsForBackwardsCompatibility(args[1:])
newArgs = append([]string{bulkExportCommandName}, newArgs...)
rootCmd.SetArgs(newArgs)
}
}

func isLegacyArgs() bool {
args := os.Args[1:]
func isLegacyArgs(args []string) bool {
args = args[1:]
if len(args) == 0 {
// a valid legacy workload was the piping of a list of assets to stdin
piped, err := parameters.IsInputPiped(os.Stdin)
Expand All @@ -118,7 +141,7 @@ func isLegacyArgs() bool {
return false
}
cmd, _, err := rootCmd.Find(args)
if err == nil && cmd.Args != nil {
if err == nil && cmd != nil {
return false
}
for i := 0; i < len(args); {
Expand Down
204 changes: 204 additions & 0 deletions pkg/cli/powertools/changestateintospec/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// Copyright 2024 Google LLC
//
// 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 changestateintospec

import (
"context"
"errors"
"fmt"
"io"
"strings"

"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/cli/powertools/diffs"
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/cli/powertools/kubecli"
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/stateintospec"
"github.com/spf13/cobra"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)

// Options configures the behaviour of the ChangeStateIntoSpec operation.
type Options struct {
kubecli.ClusterOptions
kubecli.ObjectOptions

// NewStateIntoSpecAnnotation is the new value for the state-into-spec annotation
NewStateIntoSpecAnnotation string

// FieldOwner is the field-manager owner value to use when making changes
FieldOwner string

// DryRun is true if we should not actually make changes, just print the changes we would make
DryRun bool

// KeepFields is a list of additional spec fields we should preserve
KeepFields []string
}

func (o *Options) PopulateDefaults() {
o.ClusterOptions.PopulateDefaults()
o.ObjectOptions.PopulateDefaults()

o.FieldOwner = "change-state-into-spec"
o.NewStateIntoSpecAnnotation = "absent"
o.DryRun = false
}

func (o *Options) Validate() error {
var errs []error

for _, keepField := range o.KeepFields {
if !strings.HasPrefix(keepField, ".spec.") {
errs = append(errs, fmt.Errorf("unexpected keep-field flag %q, should start with .spec. (e.g. `.spec.location`)", keepField))
}
}

return errors.Join(errs...)
}

func AddCommand(parent *cobra.Command) {
var options Options
options.PopulateDefaults()

cmd := &cobra.Command{
Use: "change-state-into-spec",
Short: "Change the state-into-spec annotation on existing objects (experimental)",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

return Run(ctx, cmd.OutOrStdout(), cmd.ErrOrStderr(), options)
},
Long: `
change-state-into-spec updates the state-into-spec annotation on existing
objects in a cluster, primarily to convert from state-into-spec=merge
to state-into-spec=absent.
Examples
Change the StorageBucket "my-bucket" in the namespace "my-namespace" to
state-into-spec=absent. Fields that were set by state-into-spec=merge
will be removed. Run in dry-run: don't actually make changes, just print
the changes that would be made:
config-connector powertools change-state-into-spec \
--namespace=my-namespace --name=my-bucket \
--kind=StorageBucket \
--dry-run=true
As before, but apply the changes. Additionally, use --keep-field
to ensure that .spec.location and .spec.resourceID are not removed.
config-connector powertools change-state-into-spec \
--namespace=my-namespace --name=my-bucket \
--kind=StorageBucket \
--keep-field=.spec.location \
--keep-field=.spec.resourceID
`,
Args: cobra.ArbitraryArgs,
}

options.ObjectOptions.AddFlags(cmd)
options.ClusterOptions.AddFlags(cmd)

cmd.Flags().StringVar(&options.NewStateIntoSpecAnnotation, "set", options.NewStateIntoSpecAnnotation, "New value for the state-into-spec annotation")
cmd.Flags().BoolVar(&options.DryRun, "dry-run", options.DryRun, "dry-run mode will not make changes, but only print the changes it would make")
cmd.Flags().StringSliceVar(&options.KeepFields, "keep-field", options.KeepFields, "Additional fields to preserve in the spec, even if they were set by state-into-spec=merge (example .spec.location)")

parent.AddCommand(cmd)
}

func Run(ctx context.Context, stdout io.Writer, stderr io.Writer, options Options) error {
// log := klog.FromContext(ctx)

if err := options.Validate(); err != nil {
return err
}

if options.ImpersonateUser == "" {
// Impersonate the KCC service account, which is allowed to make changes
options.ClusterOptions.Impersonate = &rest.ImpersonationConfig{
UserName: "system:serviceaccount:cnrm-system:cnrm-controller-manager-" + options.Namespace,
Groups: []string{"system:serviceaccounts", "system:serviceaccounts:cnrm-system"},
}
} else {
options.ClusterOptions.Impersonate = &rest.ImpersonationConfig{
UserName: options.ImpersonateUser,
Groups: options.ImpersonateGroups,
}
}
kubeClient, err := kubecli.NewClient(ctx, options.ClusterOptions)
if err != nil {
return fmt.Errorf("creating client: %w", err)
}

u, err := kubeClient.GetObject(ctx, options.ObjectOptions)
if err != nil {
return fmt.Errorf("getting object: %w", err)
}

originalObject := u.DeepCopy()

keepFields := make(map[string]bool)
// The exception to state-into-spec; we always want to preserve the resourceID field.
keepFields[".spec.resourceID"] = true
for _, keepField := range options.KeepFields {
keepFields[keepField] = true
}

keepFieldFunc := func(fieldPath fieldpath.Path) bool {
fieldName := fieldPath.String()
return keepFields[fieldName]
}

warnings, err := stateintospec.RemoveStateIntoSpecFields(ctx, u, keepFieldFunc)
if err != nil {
return err
}

for _, warning := range warnings.Warnings {
fmt.Fprintf(stderr, "%v\n", warning.Message)
}

annotations := u.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
annotations["cnrm.cloud.google.com/state-into-spec"] = options.NewStateIntoSpecAnnotation
u.SetAnnotations(annotations)

diff, err := diffs.BuildObjectDiff(originalObject, u)
if err != nil {
return fmt.Errorf("building object diff: %w", err)
}

fmt.Fprintf(stdout, "\n\n")
printOpts := diffs.PrettyPrintOptions{PrintObjectInfo: true, Indent: " "}
diff.PrettyPrintTo(printOpts, stdout)
fmt.Fprintf(stdout, "\n\n")

if options.DryRun {
fmt.Fprintf(stdout, "dry-run mode, not making changes\n")
return nil
}

fmt.Fprintf(stdout, "applying changes\n")
if err := kubeClient.Update(ctx, u, client.FieldOwner(options.FieldOwner)); err != nil {
return fmt.Errorf("updating object: %w", err)
}

return nil
}
2 changes: 2 additions & 0 deletions pkg/cli/powertools/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package powertools

import (
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/cli/powertools/changestateintospec"
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/cli/powertools/forcesetfield"
"github.com/spf13/cobra"
)
Expand All @@ -28,4 +29,5 @@ func AddCommands(parent *cobra.Command) {
parent.AddCommand(powertoolsCmd)

forcesetfield.AddCommand(powertoolsCmd)
changestateintospec.AddCommand(powertoolsCmd)
}
42 changes: 19 additions & 23 deletions pkg/cli/powertools/forcesetfield/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,29 +53,20 @@ func AddCommand(parent *cobra.Command) {
options.PopulateDefaults()

cmd := &cobra.Command{
Use: "force-set-field KIND NAME FIELD.PATH=VALUE",
Use: "force-set-field FIELD.PATH=VALUE",
Short: "Sets a field on a KCC object, even immutable fields (experimental)",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

if len(args) >= 1 {
options.Kind = args[0]
}
if len(args) >= 2 {
options.Name = args[1]
}

setFields := map[string]string{}
if len(args) >= 3 {
for i := 2; i < len(args); i++ {
tokens := strings.SplitN(args[i], "=", 2)
if len(tokens) < 2 {
return fmt.Errorf("expected spec.path=value, got %q", args[i])
}
k := tokens[0]
v := tokens[1]
setFields[k] = v
for i := 0; i < len(args); i++ {
tokens := strings.SplitN(args[i], "=", 2)
if len(tokens) < 2 {
return fmt.Errorf("expected spec.path=value, got %q", args[i])
}
k := tokens[0]
v := tokens[1]
setFields[k] = v
}

return Run(ctx, cmd.OutOrStdout(), options, setFields)
Expand All @@ -94,13 +85,18 @@ func AddCommand(parent *cobra.Command) {
func Run(ctx context.Context, out io.Writer, options Options, setFields map[string]string) error {
// log := klog.FromContext(ctx)

// Impersonate the KCC service account, which is allowed to make changes
// TODO: Make this configurable - maybe it only works in namespaced mode?
options.ClusterOptions.Impersonate = &rest.ImpersonationConfig{
UserName: "system:serviceaccount:cnrm-system:cnrm-controller-manager-" + options.Namespace,
Groups: []string{"system:serviceaccounts", "system:serviceaccounts:cnrm-system"},
if options.ImpersonateUser == "" {
// Impersonate the KCC service account, which is allowed to make changes
options.ClusterOptions.Impersonate = &rest.ImpersonationConfig{
UserName: "system:serviceaccount:cnrm-system:cnrm-controller-manager-" + options.Namespace,
Groups: []string{"system:serviceaccounts", "system:serviceaccounts:cnrm-system"},
}
} else {
options.ClusterOptions.Impersonate = &rest.ImpersonationConfig{
UserName: options.ImpersonateUser,
Groups: options.ImpersonateGroups,
}
}

kubeClient, err := kubecli.NewClient(ctx, options.ClusterOptions)
if err != nil {
return fmt.Errorf("creating client: %w", err)
Expand Down
Loading

0 comments on commit bbdd7e2

Please sign in to comment.