Skip to content
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

Add namespace, label, and annotation support to asoctl #3884

Merged
merged 1 commit into from
Mar 26, 2024
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
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",
matthchr marked this conversation as resolved.
Show resolved Hide resolved
"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/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/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))
})
}
}
Loading