Add a package for loading Downward API metadata.
Signed-off-by: Matt Moyer <moyerm@vmware.com>
This commit is contained in:
parent
da4f036622
commit
a01970602a
70
internal/downward/downward.go
Normal file
70
internal/downward/downward.go
Normal file
@ -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
|
||||||
|
}
|
111
internal/downward/downward_test.go
Normal file
111
internal/downward/downward_test.go
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
1
internal/downward/testdata/invalidlabels/labels
vendored
Normal file
1
internal/downward/testdata/invalidlabels/labels
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
invalid
|
1
internal/downward/testdata/invalidlabels/namespace
vendored
Normal file
1
internal/downward/testdata/invalidlabels/namespace
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
test-namespace
|
1
internal/downward/testdata/missinglabels/namespace
vendored
Normal file
1
internal/downward/testdata/missinglabels/namespace
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
test-namespace
|
2
internal/downward/testdata/valid/labels
vendored
Normal file
2
internal/downward/testdata/valid/labels
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
foo="bar"
|
||||||
|
bat="baz"
|
1
internal/downward/testdata/valid/namespace
vendored
Normal file
1
internal/downward/testdata/valid/namespace
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
test-namespace
|
Loading…
Reference in New Issue
Block a user