Skip to content

Commit

Permalink
Implement Addons (#782)
Browse files Browse the repository at this point in the history
* Add the Addons API

* Update the API codegen

* Addons implementation

* Ignore SetDefaults_Addons lint error

* Use logger instead of fmt

* Address code review comments

* Add unit tests

* Simplify applyAddons function

* Normalize the addons path

* Update example config

* Fix path handling

* Comment example
  • Loading branch information
xmudrii authored Feb 11, 2020
1 parent 1068c77 commit df79794
Show file tree
Hide file tree
Showing 23 changed files with 606 additions and 0 deletions.
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ linters-settings:
issues:
exclude:
- "`defaultRetryBackoff` - `retries` always receives `3`"
- "func SetDefaults_Addons should be SetDefaultsAddons"
- "func SetDefaults_APIEndpoints should be SetDefaultsAPIEndpoints"
- "func SetDefaults_ClusterNetwork should be SetDefaultsClusterNetwork"
- "func SetDefaults_Features should be SetDefaultsFeatures"
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ module github.com/kubermatic/kubeone
go 1.13

require (
github.com/Masterminds/goutils v1.1.0 // indirect
github.com/Masterminds/semver v1.4.2
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/aws/aws-sdk-go v1.20.15
github.com/ghodss/yaml v1.0.0
github.com/go-logr/zapr v0.1.1 // indirect
github.com/huandu/xstrings v1.3.0 // indirect
github.com/imdario/mergo v0.3.7
github.com/koron-go/prefixw v0.0.0-20181013140428-271b207a7572
github.com/kr/fs v0.1.0 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/pkg/errors v0.8.1
github.com/pkg/sftp v1.10.0
github.com/pmezard/go-difflib v1.0.0
Expand Down
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbt
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
Expand Down Expand Up @@ -142,6 +146,7 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
Expand All @@ -161,6 +166,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.3.0 h1:gvV6jG9dTgFEncxo+AF7PH6MZXi/vZl25owA/8Dg8Wo=
github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
Expand Down Expand Up @@ -199,8 +206,12 @@ github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand Down
81 changes: 81 additions & 0 deletions pkg/addons/addons.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
Copyright 2020 The KubeOne Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package addons

import (
"fmt"

"github.com/pkg/errors"

kubeoneapi "github.com/kubermatic/kubeone/pkg/apis/kubeone"
"github.com/kubermatic/kubeone/pkg/credentials"
"github.com/kubermatic/kubeone/pkg/runner"
"github.com/kubermatic/kubeone/pkg/ssh"
"github.com/kubermatic/kubeone/pkg/state"
)

const (
addonLabel = "kubeone.io/addon"

kubectlApplyScript = `kubectl apply -f {{.FILE_NAME}} --prune -l "%s"`
)

// TemplateData is data available in the addons render template
type TemplateData struct {
Config *kubeoneapi.KubeOneCluster
Credentials map[string]string
}

func Ensure(s *state.State) error {
if s.Cluster.Addons == nil || !s.Cluster.Addons.Enable {
s.Logger.Infoln("Skipping applying addons because addons are not enabled…")
return nil
}
s.Logger.Infoln("Applying addons…")

creds, err := credentials.ProviderCredentials(s.Cluster.CloudProvider.Name, s.CredentialsFilePath)
if err != nil {
return errors.Wrap(err, "unable to fetch credentials")
}
templateData := TemplateData{
Config: s.Cluster,
Credentials: creds,
}
if err := getManifests(s, templateData); err != nil {
return errors.WithStack(err)
}

if err := applyAddons(s); 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),
})
return err
}
180 changes: 180 additions & 0 deletions pkg/addons/manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
Copyright 2020 The KubeOne Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package addons

import (
"bufio"
"bytes"
"io"
"io/ioutil"
"path/filepath"
"strings"
"text/template"

"github.com/Masterminds/sprig"
"github.com/ghodss/yaml"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"

"github.com/kubermatic/kubeone/pkg/state"

metav1unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
kyaml "k8s.io/apimachinery/pkg/util/yaml"
)

func getManifests(s *state.State, templateData TemplateData) 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")
}
addonsPath = filepath.Join(manifestAbsPath, addonsPath)
}

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

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

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

return nil
}

// loadAddonsManifests loads all YAML files from a given directory and runs the templating logic
func loadAddonsManifests(addonsPath string, logger logrus.FieldLogger, verbose bool, templateData TemplateData) ([]runtime.RawExtension, error) {
manifests := []runtime.RawExtension{}

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

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
}
if verbose {
logger.Infof("Parsing addons manifest '%s'\n", file.Name())
}

manifestBytes, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, errors.Wrapf(err, "failed to load addon %s", file.Name())
}

tpl, err := template.New("addons-base").Funcs(sprig.TxtFuncMap()).Parse(string(manifestBytes))
if err != nil {
return nil, errors.Wrapf(err, "failed to template addons manifest %s", file.Name())
}
buf := bytes.NewBuffer([]byte{})
if err := tpl.Execute(buf, templateData); err != nil {
return nil, errors.Wrapf(err, "failed to template addons manifest %s", file.Name())
}

trim := strings.TrimSpace(buf.String())
if len(trim) == 0 {
logger.Infof("Addons manifest '%s' is empty after parsing. Skipping.\n", file.Name())
}

reader := kyaml.NewYAMLReader(bufio.NewReader(buf))
for {
b, err := reader.Read()
if err != nil {
if err == io.EOF {
break
}
return nil, errors.Wrapf(err, "failed reading from YAML reader for manifest %s", file.Name())
}
b = bytes.TrimSpace(b)
if len(b) == 0 {
continue
}
decoder := kyaml.NewYAMLToJSONDecoder(bytes.NewBuffer(b))
raw := runtime.RawExtension{}
if err := decoder.Decode(&raw); err != nil {
return nil, errors.Wrapf(err, "failed to decode manifest %s", file.Name())
}
if len(raw.Raw) == 0 {
// This can happen if the manifest contains only comments
continue
}
manifests = append(manifests, raw)
}
}

return manifests, nil
}

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

for _, m := range manifests {
parsedUnstructuredObj := &metav1unstructured.Unstructured{}
if _, _, err := metav1unstructured.UnstructuredJSONScheme.Decode(m.Raw, nil, parsedUnstructuredObj); err != nil {
return nil, errors.Wrapf(err, "failed to parse unstructured fields")
}

existingLabels := parsedUnstructuredObj.GetLabels()
if existingLabels == nil {
existingLabels = map[string]string{}
}
existingLabels[addonLabel] = ""
parsedUnstructuredObj.SetLabels(existingLabels)

jsonBuffer := &bytes.Buffer{}
if err := metav1unstructured.UnstructuredJSONScheme.Encode(parsedUnstructuredObj, jsonBuffer); err != nil {
return nil, errors.Wrap(err, "encoding json failed")
}

// Must be encoded back to YAML, otherwise kubectl fails to apply because it tries to parse the whole
// thing as json
yamlBytes, err := yaml.JSONToYAML(jsonBuffer.Bytes())
if err != nil {
return nil, err
}

rawManifests = append(rawManifests, bytes.NewBuffer(yamlBytes))
}

return rawManifests, nil
}

// combineManifests combines all manifest into a single one.
// This is needed so we can properly utilize kubectl apply --prune
func combineManifests(manifests []*bytes.Buffer) *bytes.Buffer {
parts := make([]string, len(manifests))
for i, m := range manifests {
s := m.String()
s = strings.TrimSuffix(s, "\n")
s = strings.TrimSpace(s)
parts[i] = s
}

return bytes.NewBufferString(strings.Join(parts, "\n---\n") + "\n")
}
Loading

0 comments on commit df79794

Please sign in to comment.