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

Support organizing addons into subdirectories #1364

Merged
merged 3 commits into from
Jun 3, 2021
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
72 changes: 51 additions & 21 deletions pkg/addons/addons.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,26 @@ package addons

import (
"fmt"
"strings"

"github.com/MakeNowJust/heredoc/v2"
"github.com/pkg/errors"

kubeoneapi "k8c.io/kubeone/pkg/apis/kubeone"
"k8c.io/kubeone/pkg/credentials"
"k8c.io/kubeone/pkg/runner"
"k8c.io/kubeone/pkg/ssh"
"k8c.io/kubeone/pkg/state"
)

const (
addonLabel = "kubeone.io/addon"
kubectlApplyScript = `
sudo KUBECONFIG=/etc/kubernetes/admin.conf \
kubectl apply -f {{.FILE_NAME}} --prune -l "%s"
`
addonLabel = "kubeone.io/addon"
)

var (
kubectlApplyScript = heredoc.Doc(`
sudo KUBECONFIG=/etc/kubernetes/admin.conf \
kubectl apply -f - --prune -l "%s=%s"
`)
)

// TemplateData is data available in the addons render template
Expand All @@ -58,27 +62,53 @@ func Ensure(s *state.State) error {
Config: s.Cluster,
Credentials: creds,
}
if err := getManifests(s, templateData); err != nil {
return errors.WithStack(err)

addonsPath, dirs, err := traverseAddonsDirectory(s)
if err != nil {
return errors.Wrap(err, "failed to parse the addons directory")
}

if err := applyAddons(s); err != nil {
return errors.Wrap(err, "failed to apply addons")
for _, addonDir := range dirs {
if len(addonDir) == 0 {
s.Logger.Info("Applying addons from the root directory...")
} else {
s.Logger.Infof("Applying addon %q...", addonDir)
}

manifest, err := getManifestsFromDirectory(s, templateData, addonsPath, addonDir)
if err != nil {
return errors.WithStack(err)
}
if len(strings.TrimSpace(manifest)) == 0 {
if len(addonDir) != 0 {
s.Logger.Warnf("Addon directory %q is empty, skipping...", addonDir)
}
continue
}

if err := applyAddons(s, manifest, addonDir); err != nil {
return errors.Wrap(err, "failed to apply addons")
}
}

return nil
}

func applyAddons(s *state.State) error {
return errors.Wrap(s.RunTaskOnLeader(runKubectl), "failed to apply addons")
}

func runKubectl(s *state.State, _ *kubeoneapi.HostConfig, _ ssh.Connection) error {
if err := s.Configuration.UploadTo(s.Runner.Conn, s.WorkDir); err != nil {
return errors.Wrap(err, "failed to upload manifests")
}
_, _, err := s.Runner.Run(fmt.Sprintf(kubectlApplyScript, addonLabel), runner.TemplateVariables{
"FILE_NAME": fmt.Sprintf("%s/addons/", s.WorkDir),
func applyAddons(s *state.State, manifest string, addonName string) error {
return s.RunTaskOnLeader(func(s *state.State, _ *kubeoneapi.HostConfig, conn ssh.Connection) error {
var (
cmd = fmt.Sprintf(kubectlApplyScript, addonLabel, addonName)
stdin = strings.NewReader(manifest)
stdout, stderr strings.Builder
)

_, err := conn.POpen(cmd, stdin, &stdout, &stderr)
if s.Verbose {
fmt.Printf("+ %s\n", cmd)
fmt.Printf("%s", stderr.String())
fmt.Printf("%s", stdout.String())
}

return err
})
return err
}
43 changes: 33 additions & 10 deletions pkg/addons/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,35 +39,59 @@ import (
"sigs.k8s.io/yaml"
)

func getManifests(s *state.State, templateData TemplateData) error {
func traverseAddonsDirectory(s *state.State) (string, []string, error) {
addonsPath := s.Cluster.Addons.Path
if !filepath.IsAbs(addonsPath) && s.ManifestFilePath != "" {
manifestAbsPath, err := filepath.Abs(filepath.Dir(s.ManifestFilePath))
if err != nil {
return errors.Wrap(err, "unable to get absolute path to the cluster manifest")
return "", nil, errors.Wrap(err, "unable to get absolute path to the cluster manifest")
}
addonsPath = filepath.Join(manifestAbsPath, addonsPath)
}

dirInfo, err := ioutil.ReadDir(addonsPath)
if err != nil {
return "", nil, errors.Wrapf(err, "failed to read the addons directory %s", addonsPath)
}

var dirs []string
for _, d := range dirInfo {
if d.IsDir() {
dirs = append(dirs, d.Name())
}
}

// We're doing this to support legacy addons where all addons are in the
// root directory.
// The root directory is intentionally applied at the end in order to
// relabel manifests that are moved from the addons directory to a
// subdirectory.
dirs = append(dirs, "")

return addonsPath, dirs, nil
}

func getManifestsFromDirectory(s *state.State, templateData TemplateData, addonsPath, directory string) (string, error) {
addonsPath = filepath.Join(addonsPath, directory)

overwriteRegistry := ""
if s.Cluster.RegistryConfiguration != nil && s.Cluster.RegistryConfiguration.OverwriteRegistry != "" {
overwriteRegistry = s.Cluster.RegistryConfiguration.OverwriteRegistry
}

manifests, err := loadAddonsManifests(addonsPath, s.Logger, s.Verbose, templateData, overwriteRegistry)
if err != nil {
return err
return "", err
}

rawManifests, err := ensureAddonsLabelsOnResources(manifests)
rawManifests, err := ensureAddonsLabelsOnResources(manifests, directory)
if err != nil {
return err
return "", err
}

combinedManifests := combineManifests(rawManifests)
s.Configuration.AddFile("addons/addons.yaml", combinedManifests.String())

return nil
return combinedManifests.String(), nil
}

// loadAddonsManifests loads all YAML files from a given directory and runs the templating logic
Expand All @@ -82,7 +106,6 @@ func loadAddonsManifests(addonsPath string, logger logrus.FieldLogger, verbose b
for _, file := range files {
filePath := filepath.Join(addonsPath, file.Name())
if file.IsDir() {
logger.Infof("Found directory '%s' in the addons path. Ignoring.\n", file.Name())
continue
}
ext := strings.ToLower(filepath.Ext(filePath))
Expand Down Expand Up @@ -148,7 +171,7 @@ func loadAddonsManifests(addonsPath string, logger logrus.FieldLogger, verbose b
}

// ensureAddonsLabelsOnResources applies the addons label on all resources in the manifest
func ensureAddonsLabelsOnResources(manifests []runtime.RawExtension) ([]*bytes.Buffer, error) {
func ensureAddonsLabelsOnResources(manifests []runtime.RawExtension, addonName string) ([]*bytes.Buffer, error) {
var rawManifests []*bytes.Buffer

for _, m := range manifests {
Expand All @@ -161,7 +184,7 @@ func ensureAddonsLabelsOnResources(manifests []runtime.RawExtension) ([]*bytes.B
if existingLabels == nil {
existingLabels = map[string]string{}
}
existingLabels[addonLabel] = ""
existingLabels[addonLabel] = addonName
parsedUnstructuredObj.SetLabels(existingLabels)

jsonBuffer := &bytes.Buffer{}
Expand Down
96 changes: 68 additions & 28 deletions pkg/addons/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ metadata:
namespace: kube-system
`

testManifest1WithLabel = `apiVersion: v1
testManifest1WithEmptyLabel = `apiVersion: v1
data:
foo: bar
kind: ConfigMap
Expand All @@ -87,6 +87,19 @@ metadata:
name: test1
namespace: kube-system
`

testManifest1WithNamedLabel = `apiVersion: v1
data:
foo: bar
kind: ConfigMap
metadata:
labels:
app: test
cluster: kubeone-test
kubeone.io/addon: test-addon
name: test1
namespace: kube-system
`
)

const (
Expand Down Expand Up @@ -139,37 +152,64 @@ var (
)

func TestEnsureAddonsLabelsOnResources(t *testing.T) {
addonsDir, err := ioutil.TempDir("/tmp", "kubeone")
if err != nil {
t.Fatalf("unable to create temporary addons directory: %v", err)
}
defer os.RemoveAll(addonsDir)

if writeErr := ioutil.WriteFile(path.Join(addonsDir, "testManifest.yaml"), []byte(testManifest1WithoutLabel), 0600); writeErr != nil {
t.Fatalf("unable to create temporary addon manifest: %v", err)
}
t.Parallel()

templateData := TemplateData{
Config: &kubeoneapi.KubeOneCluster{
Name: "kubeone-test",
tests := []struct {
name string
addonName string
addonManifest string
expectedManifest string
}{
{
name: "addon with no name (root directory addons)",
addonName: "",
addonManifest: testManifest1WithoutLabel,
expectedManifest: testManifest1WithEmptyLabel,
},
{
name: "addon with name",
addonName: "test-addon",
addonManifest: testManifest1WithoutLabel,
expectedManifest: testManifest1WithNamedLabel,
},
}
manifests, err := loadAddonsManifests(addonsDir, nil, false, templateData, "")
if err != nil {
t.Fatalf("unable to load manifests: %v", err)
}
if len(manifests) != 1 {
t.Fatalf("expected to load 1 manifest, got %d", len(manifests))
}

b, err := ensureAddonsLabelsOnResources(manifests)
if err != nil {
t.Fatalf("unable to ensure labels: %v", err)
}
manifest := b[0].String()
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
addonsDir, err := ioutil.TempDir("/tmp", "kubeone")
if err != nil {
t.Fatalf("unable to create temporary addons directory: %v", err)
}
defer os.RemoveAll(addonsDir)

if manifest != testManifest1WithLabel {
t.Fatalf("invalid manifest returned. expected \n%s, got \n%s", testManifest1WithLabel, manifest)
if writeErr := ioutil.WriteFile(path.Join(addonsDir, "testManifest.yaml"), []byte(tc.addonManifest), 0600); writeErr != nil {
t.Fatalf("unable to create temporary addon manifest: %v", err)
}

templateData := TemplateData{
Config: &kubeoneapi.KubeOneCluster{
Name: "kubeone-test",
},
}
manifests, err := loadAddonsManifests(addonsDir, nil, false, templateData, "")
if err != nil {
t.Fatalf("unable to load manifests: %v", err)
}
if len(manifests) != 1 {
t.Fatalf("expected to load 1 manifest, got %d", len(manifests))
}

b, err := ensureAddonsLabelsOnResources(manifests, tc.addonName)
if err != nil {
t.Fatalf("unable to ensure labels: %v", err)
}
manifest := b[0].String()

if manifest != tc.expectedManifest {
t.Fatalf("invalid manifest returned. expected \n%s, got \n%s", tc.expectedManifest, manifest)
}
})
}
}

Expand Down Expand Up @@ -242,7 +282,7 @@ func TestImageRegistryParsing(t *testing.T) {
t.Fatalf("expected to load 1 manifest, got %d", len(manifests))
}

b, err := ensureAddonsLabelsOnResources(manifests)
b, err := ensureAddonsLabelsOnResources(manifests, "")
if err != nil {
t.Fatalf("unable to ensure labels: %v", err)
}
Expand Down