From a01970602a9cfd778e048dfb92207b6ea0db7824 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Wed, 15 Jul 2020 15:38:39 -0500 Subject: [PATCH] Add a package for loading Downward API metadata. Signed-off-by: Matt Moyer --- internal/downward/downward.go | 70 +++++++++++ internal/downward/downward_test.go | 111 ++++++++++++++++++ .../downward/testdata/invalidlabels/labels | 1 + .../downward/testdata/invalidlabels/namespace | 1 + .../downward/testdata/missinglabels/namespace | 1 + internal/downward/testdata/valid/labels | 2 + internal/downward/testdata/valid/namespace | 1 + 7 files changed, 187 insertions(+) create mode 100644 internal/downward/downward.go create mode 100644 internal/downward/downward_test.go create mode 100644 internal/downward/testdata/invalidlabels/labels create mode 100644 internal/downward/testdata/invalidlabels/namespace create mode 100644 internal/downward/testdata/missinglabels/namespace create mode 100644 internal/downward/testdata/valid/labels create mode 100644 internal/downward/testdata/valid/namespace diff --git a/internal/downward/downward.go b/internal/downward/downward.go new file mode 100644 index 00000000..47ee73fb --- /dev/null +++ b/internal/downward/downward.go @@ -0,0 +1,70 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +// Package downward implements a client interface for interacting with Kubernetes "downwardAPI" volumes. +// See https://kubernetes.io/docs/tasks/inject-data-application/downward-api-volume-expose-pod-information/. +package downward + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "path/filepath" + "strconv" + "strings" +) + +// PodInfo contains pod metadata about the current pod. +type PodInfo struct { + // Namespace where the current pod is running. + Namespace string + + // Labels of the current pod. + Labels map[string]string +} + +// Load pod metadata from a downwardAPI volume directory. +func Load(directory string) (*PodInfo, error) { + var result PodInfo + ns, err := ioutil.ReadFile(filepath.Join(directory, "namespace")) + if err != nil { + return nil, fmt.Errorf("could not load namespace: %w", err) + } + result.Namespace = strings.TrimSpace(string(ns)) + + labels, err := ioutil.ReadFile(filepath.Join(directory, "labels")) + if err != nil { + return nil, fmt.Errorf("could not load labels: %w", err) + } + result.Labels, err = parseMap(labels) + if err != nil { + return nil, fmt.Errorf("could not parse labels: %w", err) + } + return &result, nil +} + +// parseMap parses the key/value format emitted by the Kubernetes Downward API for pod labels and annotations. +// See https://kubernetes.io/docs/tasks/inject-data-application/downward-api-volume-expose-pod-information/. +// See https://github.com/kubernetes/kubernetes/blob/4b2cb072dba10227083b16731f019f096c581787/pkg/fieldpath/fieldpath.go#L28. +func parseMap(input []byte) (map[string]string, error) { + result := map[string]string{} + for _, line := range bytes.Split(input, []byte("\n")) { + line = bytes.TrimSpace(line) + if len(line) == 0 { + continue + } + parts := bytes.SplitN(line, []byte("="), 2) + if len(parts) != 2 { + return nil, fmt.Errorf("expected 2 parts, found %d: %w", len(parts), io.ErrShortBuffer) + } + value, err := strconv.Unquote(string(parts[1])) + if err != nil { + return nil, fmt.Errorf("invalid quoted value: %w", err) + } + result[string(parts[0])] = value + } + return result, nil +} diff --git a/internal/downward/downward_test.go b/internal/downward/downward_test.go new file mode 100644 index 00000000..048d5657 --- /dev/null +++ b/internal/downward/downward_test.go @@ -0,0 +1,111 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package downward + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLoad(t *testing.T) { + t.Parallel() + tests := []struct { + name string + inputDir string + wantErr string + want *PodInfo + }{ + { + name: "missing directory", + inputDir: "./testdata/no-such-directory", + wantErr: "could not load namespace: open testdata/no-such-directory/namespace: no such file or directory", + }, + { + name: "missing labels file", + inputDir: "./testdata/missinglabels", + wantErr: "could not load labels: open testdata/missinglabels/labels: no such file or directory", + }, + { + name: "invalid labels file", + inputDir: "./testdata/invalidlabels", + wantErr: "could not parse labels: expected 2 parts, found 1: short buffer", + }, + { + name: "valid", + inputDir: "./testdata/valid", + want: &PodInfo{ + Namespace: "test-namespace", + Labels: map[string]string{"foo": "bar", "bat": "baz"}, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := Load(tt.inputDir) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + require.Empty(t, got) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} + +func TestParseMap(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input []byte + wantErr string + want map[string]string + }{ + { + name: "empty", + want: map[string]string{}, + }, + { + name: "missing equal", + input: []byte(`akjhlakjh`), + wantErr: "expected 2 parts, found 1: short buffer", + }, + { + name: "missing invalid value", + input: []byte(`akjhlakjh="foo\qbar"`), + wantErr: "invalid quoted value: invalid syntax", + }, + { + name: "success", + input: []byte(` +fooTime="2020-07-15T19:35:12.027636555Z" +example.com/config.source="api" +example.com/bar="baz\x01" +`), + want: map[string]string{ + "fooTime": "2020-07-15T19:35:12.027636555Z", + "example.com/config.source": "api", + "example.com/bar": "baz\x01", + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := parseMap(tt.input) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + require.Empty(t, got) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/downward/testdata/invalidlabels/labels b/internal/downward/testdata/invalidlabels/labels new file mode 100644 index 00000000..e466dcbd --- /dev/null +++ b/internal/downward/testdata/invalidlabels/labels @@ -0,0 +1 @@ +invalid \ No newline at end of file diff --git a/internal/downward/testdata/invalidlabels/namespace b/internal/downward/testdata/invalidlabels/namespace new file mode 100644 index 00000000..d2826d00 --- /dev/null +++ b/internal/downward/testdata/invalidlabels/namespace @@ -0,0 +1 @@ +test-namespace \ No newline at end of file diff --git a/internal/downward/testdata/missinglabels/namespace b/internal/downward/testdata/missinglabels/namespace new file mode 100644 index 00000000..d2826d00 --- /dev/null +++ b/internal/downward/testdata/missinglabels/namespace @@ -0,0 +1 @@ +test-namespace \ No newline at end of file diff --git a/internal/downward/testdata/valid/labels b/internal/downward/testdata/valid/labels new file mode 100644 index 00000000..433404c1 --- /dev/null +++ b/internal/downward/testdata/valid/labels @@ -0,0 +1,2 @@ +foo="bar" +bat="baz" \ No newline at end of file diff --git a/internal/downward/testdata/valid/namespace b/internal/downward/testdata/valid/namespace new file mode 100644 index 00000000..d2826d00 --- /dev/null +++ b/internal/downward/testdata/valid/namespace @@ -0,0 +1 @@ +test-namespace \ No newline at end of file