-
Notifications
You must be signed in to change notification settings - Fork 242
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
23 changed files
with
606 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
Oops, something went wrong.