Skip to content

Commit

Permalink
Add namespace, label, and annotation support to asoctl
Browse files Browse the repository at this point in the history
Fixes #3865
  • Loading branch information
matthchr committed Mar 25, 2024
1 parent dc6e229 commit a9d8aab
Show file tree
Hide file tree
Showing 6 changed files with 325 additions and 0 deletions.
41 changes: 41 additions & 0 deletions v2/cmd/asoctl/cmd/import_azure_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,25 @@ func newImportAzureResourceCommand() *cobra.Command {

cmd.MarkFlagsMutuallyExclusive("output", "output-folder")

cmd.Flags().StringVarP(
&options.namespace,
"namespace",
"n",
"",
"Write the imported resources to the specified namespace")
cmd.Flags().StringSliceVarP(
&options.labels,
"label",
"l",
nil,
"Add the specified labels to the imported resources. Multiple comma-separated labels can be specified (--label example.com/mylabel=foo,example.com/mylabel2=bar) or the --label (-l) argument can be used multiple times (-l example.com/mylabel=foo -l example.com/mylabel2=bar)")
cmd.Flags().StringSliceVarP(
&options.annotations,
"annotation",
"a",
nil,
"Add the specified annotations to the imported resources. Multiple comma-separated annotations can be specified (--annotation example.com/myannotation=foo,example.com/myannotation2=bar) or the --annotation (-a) argument can be used multiple times (-a example.com/myannotation=foo -a example.com/myannotation2=bar)")

return cmd
}

Expand Down Expand Up @@ -98,6 +117,25 @@ func importAzureResource(ctx context.Context, armIDs []string, options importAzu
return nil
}

// Apply additional configuration to imported resources.
if options.namespace != "" {
result.SetNamespace(options.namespace)
}

if len(options.labels) > 0 {
err = result.AddLabels(options.labels)
if err != nil {
return errors.Wrap(err, "failed to add labels")
}
}

if len(options.annotations) > 0 {
err = result.AddAnnotations(options.annotations)
if err != nil {
return errors.Wrap(err, "failed to add annotations")
}
}

if file, ok := options.writeToFile(); ok {
log.Info(
"Writing to a single file",
Expand Down Expand Up @@ -130,6 +168,9 @@ func importAzureResource(ctx context.Context, armIDs []string, options importAzu
type importAzureResourceOptions struct {
outputPath *string
outputFolder *string
namespace string
annotations []string
labels []string
}

func (option *importAzureResourceOptions) writeToFile() (string, bool) {
Expand Down
53 changes: 53 additions & 0 deletions v2/cmd/asoctl/internal/importing/resource_import_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"golang.org/x/exp/slices"
"sigs.k8s.io/yaml"

"github.com/Azure/azure-service-operator/v2/internal/annotations"
"github.com/Azure/azure-service-operator/v2/internal/labels"
"github.com/Azure/azure-service-operator/v2/pkg/genruntime"
)

Expand All @@ -38,6 +40,57 @@ func (r *ResourceImportResult) SaveToSingleFile(filepath string) error {
return r.saveTo(r.resources, filepath)
}

// AddAnnotations adds the given annotations to all the resources
func (r *ResourceImportResult) AddAnnotations(toAdd []string) error {
// pre-parse the annotations
parsed, err := annotations.ParseAll(toAdd)
if err != nil {
return err
}

for _, resource := range r.resources {
anntns := resource.GetAnnotations()
if anntns == nil {
anntns = make(map[string]string, len(toAdd))
}
for _, annotation := range parsed {
anntns[annotation.Key] = annotation.Value
}
resource.SetAnnotations(anntns)
}

return nil
}

// AddLabels adds the given labels to all the resources
func (r *ResourceImportResult) AddLabels(toAdd []string) error {
// pre-parse the labels
parsed, err := labels.ParseAll(toAdd)
if err != nil {
return err
}

for _, resource := range r.resources {
lbls := resource.GetLabels()
if lbls == nil {
lbls = make(map[string]string, len(toAdd))
}
for _, label := range parsed {
lbls[label.Key] = label.Value
}
resource.SetLabels(lbls)
}

return nil
}

// SetNamespace sets the namespace for all the resources
func (r *ResourceImportResult) SetNamespace(namespace string) {
for _, resource := range r.resources {
resource.SetNamespace(namespace)
}
}

func (r *ResourceImportResult) SaveToIndividualFilesInFolder(folder string) error {
// We name the files after the resource type and name
// We allocate resources to files using a map, just in case we have a naming collision
Expand Down
79 changes: 79 additions & 0 deletions v2/internal/annotations/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/

package annotations

import (
"strings"

"github.com/pkg/errors"
)

type Annotation struct {
Key string
Value string
}

// Parse parses an annotation. Amazingly there doesn't seem to be a function in client-go or similar that does this
func Parse(s string) (Annotation, error) {
split := strings.Split(s, "=")
if len(split) != 2 {
return Annotation{}, errors.Errorf("%s must have two parts separated by '='", s)
}

// see https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set
key := split[0]
value := split[1]

if len(key) == 0 {
return Annotation{}, errors.New("key can't be length 0")
}

keySplit := strings.Split(key, "/")
if len(keySplit) > 2 {
return Annotation{}, errors.Errorf("key %s must contain only a single '/'", key)
}

var name string
var prefix string
if len(keySplit) == 1 {
name = key
} else {
// Len == 2
prefix = keySplit[0]
name = keySplit[1]

}

if len(key) > 63 {
return Annotation{}, errors.Errorf("name %s must be 63 characters or less", name)
}

if len(prefix) > 253 {
return Annotation{}, errors.Errorf("prefix %s must be 253 characters or less", prefix)
}

// TODO: Could enforce character restrictions too but not bothering for now

return Annotation{
Key: key,
Value: value,
}, nil
}

// ParseAll parses all the given annotations and returns a collection of parsed annotations
func ParseAll(annotations []string) ([]Annotation, error) {
result := make([]Annotation, 0, len(annotations))

for _, annotation := range annotations {
parsed, err := Parse(annotation)
if err != nil {
return nil, errors.Wrapf(err, "failed parsing %s", annotation)
}
result = append(result, parsed)
}

return result, nil
}
52 changes: 52 additions & 0 deletions v2/internal/annotations/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/

package annotations_test

import (
"testing"

. "github.com/onsi/gomega"

"github.com/Azure/azure-service-operator/v2/internal/util/annotations"
)

func TestParse(t *testing.T) {
t.Parallel()

tests := []struct {
annotation string
wantKey string
wantValue string
wantErr bool
}{
{"example.com/annotation=value", "example.com/annotation", "value", false},
{"example.com/annotation=", "example.com/annotation", "", false},
{"=value", "", "", true},
{"example.com/annotation", "", "", true},
{"example.com/test/annotation", "", "", true},
{"thisisaverylongannotationname_solonginfactthatitisgoingtocauseanerror", "", "", true},
{"", "", "", true},
}

for _, tt := range tests {
tt := tt
t.Run(tt.annotation, func(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

actual, err := annotations.Parse(tt.annotation)

if tt.wantErr {
g.Expect(err).To(HaveOccurred())
} else {
g.Expect(err).ToNot(HaveOccurred())
}

g.Expect(actual.Key).To(Equal(tt.wantKey))
g.Expect(actual.Value).To(Equal(tt.wantValue))
})
}
}
48 changes: 48 additions & 0 deletions v2/internal/labels/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/

package labels

import (
"github.com/pkg/errors"

"github.com/Azure/azure-service-operator/v2/internal/annotations"
)

type Label struct {
Key string
Value string
}

// Parse parses a label. Amazingly there doesn't seem to be a function in client-go or similar that does this.
// There does exist an apimachinery labels.Parse but it parses label selectors not labels themselves.
func Parse(s string) (Label, error) {
// Currently the label restrictions are exactly the same as annotations,
// so we can just re-use annotation parse here
annotation, err := annotations.Parse(s)
if err != nil {
return Label{}, err
}

return Label{
Key: annotation.Key,
Value: annotation.Value,
}, nil
}

// ParseAll parses all the given labels and returns a collection of parsed labels
func ParseAll(labels []string) ([]Label, error) {
result := make([]Label, 0, len(labels))

for _, label := range labels {
parsed, err := Parse(label)
if err != nil {
return nil, errors.Wrapf(err, "failed parsing %s", label)
}
result = append(result, parsed)
}

return result, nil
}
52 changes: 52 additions & 0 deletions v2/internal/labels/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/

package labels_test

import (
"testing"

. "github.com/onsi/gomega"

"github.com/Azure/azure-service-operator/v2/internal/util/labels"
)

func TestParse(t *testing.T) {
t.Parallel()

tests := []struct {
label string
wantKey string
wantValue string
wantErr bool
}{
{"example.com/label=value", "example.com/label", "value", false},
{"example.com/label=", "example.com/label", "", false},
{"=value", "", "", true},
{"example.com/label", "", "", true},
{"example.com/test/label", "", "", true},
{"thisisaverylonglabelname_solonginfactthatitisgoingtocauseanerror", "", "", true},
{"", "", "", true},
}

for _, tt := range tests {
tt := tt
t.Run(tt.label, func(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

actual, err := labels.Parse(tt.label)

if tt.wantErr {
g.Expect(err).To(HaveOccurred())
} else {
g.Expect(err).ToNot(HaveOccurred())
}

g.Expect(actual.Key).To(Equal(tt.wantKey))
g.Expect(actual.Value).To(Equal(tt.wantValue))
})
}
}

0 comments on commit a9d8aab

Please sign in to comment.