mirror of https://github.com/fluxcd/flux2.git
Add a build kustomization feature
If implemented it will permit queriying the Kubernetes API to fetch the specified Flux Kustomization, then uses the specified path to build the overlay. Signed-off-by: Soule BA <soule@weave.works>pull/2167/head
parent
63e54f3575
commit
70fb87bc93
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Flux 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var buildCmd = &cobra.Command{
|
||||||
|
Use: "build",
|
||||||
|
Short: "Build a flux resource",
|
||||||
|
Long: "The build command is used to build flux resources.",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(buildCmd)
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Flux 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/internal/kustomization"
|
||||||
|
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var buildKsCmd = &cobra.Command{
|
||||||
|
Use: "kustomization",
|
||||||
|
Aliases: []string{"ks"},
|
||||||
|
Short: "Build Kustomization",
|
||||||
|
Long: `The build command queries the Kubernetes API and fetches the specified Flux Kustomization,
|
||||||
|
then it uses the specified files or path to build the overlay to write the resulting multi-doc YAML to stdout.`,
|
||||||
|
Example: `# Create a new overlay.
|
||||||
|
flux build kustomization my-app --resources ./path/to/local/manifests`,
|
||||||
|
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
|
||||||
|
RunE: buildKsCmdRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
type buildKsFlags struct {
|
||||||
|
resources string
|
||||||
|
}
|
||||||
|
|
||||||
|
var buildKsArgs buildKsFlags
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
buildKsCmd.Flags().StringVar(&buildKsArgs.resources, "resources", "", "Name of a file containing a file to add to the kustomization file.)")
|
||||||
|
buildCmd.AddCommand(buildKsCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildKsCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) < 1 {
|
||||||
|
return fmt.Errorf("%s name is required", kustomizationType.humanKind)
|
||||||
|
}
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
if buildKsArgs.resources == "" {
|
||||||
|
return fmt.Errorf("invalid resource path %q", buildKsArgs.resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fs, err := os.Stat(buildKsArgs.resources); err != nil || !fs.IsDir() {
|
||||||
|
return fmt.Errorf("invalid resource path %q", buildKsArgs.resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder, err := kustomization.NewBuilder(rootArgs.kubeconfig, rootArgs.kubecontext, rootArgs.namespace, name, buildKsArgs.resources, kustomization.WithTimeout(rootArgs.timeout))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
manifests, err := builder.Build()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Print(string(manifests))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,228 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Flux 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 kustomization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/internal/utils"
|
||||||
|
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/kustomize/api/konfig"
|
||||||
|
"sigs.k8s.io/kustomize/api/resmap"
|
||||||
|
"sigs.k8s.io/kustomize/api/resource"
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/filesys"
|
||||||
|
)
|
||||||
|
|
||||||
|
const mask string = "**SOPS**"
|
||||||
|
|
||||||
|
var defaultTimeout = 80 * time.Second
|
||||||
|
|
||||||
|
// Builder builds yaml manifests
|
||||||
|
// It retrieves the kustomization object from the k8s cluster
|
||||||
|
// and overlays the manifests with the resources specified in the resourcesPath
|
||||||
|
type Builder struct {
|
||||||
|
client client.WithWatch
|
||||||
|
name string
|
||||||
|
namespace string
|
||||||
|
resourcesPath string
|
||||||
|
kustomization *kustomizev1.Kustomization
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type BuilderOptionFunc func(b *Builder) error
|
||||||
|
|
||||||
|
func WithTimeout(timeout time.Duration) BuilderOptionFunc {
|
||||||
|
return func(b *Builder) error {
|
||||||
|
b.timeout = timeout
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBuilder returns a new Builder
|
||||||
|
// to dp : create functional options
|
||||||
|
func NewBuilder(kubeconfig string, kubecontext string, namespace, name, resources string, opts ...BuilderOptionFunc) (*Builder, error) {
|
||||||
|
kubeClient, err := utils.KubeClient(kubeconfig, kubecontext)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b := &Builder{
|
||||||
|
client: kubeClient,
|
||||||
|
name: name,
|
||||||
|
namespace: namespace,
|
||||||
|
resourcesPath: resources,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
if err := opt(b); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.timeout == 0 {
|
||||||
|
b.timeout = defaultTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) getKustomization(ctx context.Context) (*kustomizev1.Kustomization, error) {
|
||||||
|
namespacedName := types.NamespacedName{
|
||||||
|
Namespace: b.namespace,
|
||||||
|
Name: b.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
k := &kustomizev1.Kustomization{}
|
||||||
|
err := b.client.Get(ctx, namespacedName, k)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return k, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build builds the yaml manifests from the kustomization object
|
||||||
|
// and overlays the manifests with the resources specified in the resourcesPath
|
||||||
|
// It expects a kustomization.yaml file in the resourcesPath, and it will
|
||||||
|
// generate a kustomization.yaml file if it doesn't exist
|
||||||
|
func (b *Builder) Build() ([]byte, error) {
|
||||||
|
m, err := b.build()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resources, err := m.AsYaml()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("kustomize build failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) build() (resmap.ResMap, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Get the kustomization object
|
||||||
|
k, err := b.getKustomization(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate kustomization.yaml if needed
|
||||||
|
saved, err := b.generate(*k, b.resourcesPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate kustomization.yaml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the kustomization
|
||||||
|
m, err := b.do(ctx, *k, b.resourcesPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure secrets are masked
|
||||||
|
for _, res := range m.Resources() {
|
||||||
|
err := trimSopsData(res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// store the kustomization object
|
||||||
|
b.kustomization = k
|
||||||
|
|
||||||
|
// restore the kustomization.yaml
|
||||||
|
err = restore(saved, b.resourcesPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to restore kustomization.yaml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath string) ([]byte, error) {
|
||||||
|
gen := NewGenerator(&kustomizeImpl{kustomization})
|
||||||
|
return gen.WriteFile(dirPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomization, dirPath string) (resmap.ResMap, error) {
|
||||||
|
fs := filesys.MakeFsOnDisk()
|
||||||
|
m, err := buildKustomization(fs, dirPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("kustomize build failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, res := range m.Resources() {
|
||||||
|
// run variable substitutions
|
||||||
|
if kustomization.Spec.PostBuild != nil {
|
||||||
|
outRes, err := substituteVariables(ctx, b.client, &kustomizeImpl{kustomization}, res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("var substitution failed for '%s': %w", res.GetName(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if outRes != nil {
|
||||||
|
_, err = m.Replace(res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimSopsData(res *resource.Resource) error {
|
||||||
|
// sopsMess is the base64 encoded mask
|
||||||
|
sopsMess := base64.StdEncoding.EncodeToString([]byte(mask))
|
||||||
|
|
||||||
|
if res.GetKind() == "Secret" {
|
||||||
|
dataMap := res.GetDataMap()
|
||||||
|
for k, v := range dataMap {
|
||||||
|
data, err := base64.StdEncoding.DecodeString(v)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(fmt.Errorf("failed to decode secret data: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Contains(data, []byte("sops")) {
|
||||||
|
dataMap[k] = sopsMess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.SetDataMap(dataMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func restore(saved []byte, dirPath string) error {
|
||||||
|
kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName())
|
||||||
|
err := os.WriteFile(kfile, saved, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to restore kustomization.yaml: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Flux 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 kustomization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"sigs.k8s.io/kustomize/api/resource"
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSanitizeResources(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
yamlStr string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "secret with sops token",
|
||||||
|
yamlStr: `apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: my-secret
|
||||||
|
type: Opaque
|
||||||
|
data:
|
||||||
|
token: |
|
||||||
|
ewoJImRhdGEiOiAiRU5DW0FFUzI1Nl9HQ00sZGF0YTpvQmU1UGxQbWZRQ1VVYzRzcUtJbW
|
||||||
|
p3PT0saXY6TUxMRVcxNVFDOWtSZFZWYWdKbnpMQ1NrMHhaR1dJcEFlVGZIenl4VDEwZz0s
|
||||||
|
dGFnOkszR2tCQ0dTK3V0NFRwazZuZGIwQ0E9PSx0eXBlOnN0cl0iLAoJInNvcHMiOiB7Cg
|
||||||
|
kJImttcyI6IG51bGwsCgkJImdjcF9rbXMiOiBudWxsLAoJCSJhenVyZV9rdiI6IG51bGws
|
||||||
|
CgkJImhjX3ZhdWx0IjogbnVsbCwKCQkiYWdlIjogWwoJCQl7CgkJCQkicmVjaXBpZW50Ij
|
||||||
|
ogImFnZTEwbGEyZ2Uwd3R2eDNxcjdkYXRxZjdyczR5bmd4c3pkYWw5MjdmczlydWthbXI4
|
||||||
|
dTJwc2hzdnR6N2NlIiwKCQkJCSJlbmMiOiAiLS0tLS1CRUdJTiBBR0UgRU5DUllQVEVEIE
|
||||||
|
ZJTEUtLS0tLVxuWVdkbExXVnVZM0o1Y0hScGIyNHViM0puTDNZeENpMCtJRmd5TlRVeE9T
|
||||||
|
QTFMMlJwWkhScksxRlNWbVlyZDFWYVxuWTBoeFdGUXpTREJzVDFrM1dqTnRZbVUxUW1saW
|
||||||
|
FESnljWGxOQ25GMVlqZE5PVGhWYlZOdk1HOXJOUzlaVVhad1xuTW5WMGJuUlVNR050ZWpG
|
||||||
|
UGJ6TTRVMlV6V2tzemVWa0tMUzB0SUdKNlVHaHhNVVYzWW1WSlRIbEpTVUpwUlZSWlxuVm
|
||||||
|
pkMFJWUmFkVTh3ZWt4WFRISXJZVXBsWWtOMmFFRUswSS9NQ0V0WFJrK2IvTjJHMUpGM3ZI
|
||||||
|
UVQyNGRTaFdZRFxudytKSVVTQTNhTGYyc3YwenIyTWRVRWRWV0JKb004blQ0RDR4VmJCT1
|
||||||
|
JEKzY2OVcrOW5EZVN3PT1cbi0tLS0tRU5EIEFHRSBFTkNSWVBURUQgRklMRS0tLS0tXG4i
|
||||||
|
CgkJCX0KCQldLAoJCSJsYXN0bW9kaWZpZWQiOiAiMjAyMS0xMS0yNlQxNjozNDo1MVoiLA
|
||||||
|
oJCSJtYWMiOiAiRU5DW0FFUzI1Nl9HQ00sZGF0YTpDT0d6ZjVZQ0hOTlA2ejRKYUVLcmpO
|
||||||
|
M004ZjUrUTF1S1VLVE1Id2ozODgvSUNtTHlpMnNTclRtajdQUCtYN005alRWd2E4d1ZnWV
|
||||||
|
RwTkxpVkp4K0xjeHF2SVhNMFR5bysvQ3UxenJmYW85OGFpQUNQOCtUU0VEaUZRTnRFdXMy
|
||||||
|
M0grZC9YMWhxTXdSSERJM2tRKzZzY2dFR25xWTU3cjNSRFNBM0U4RWhIcjQ9LGl2Okx4aX
|
||||||
|
RWSVltOHNyWlZxRnVlSmg5bG9DbEE0NFkyWjNYQVZZbXhlc01tT2c9LHRhZzpZOHFGRDhV
|
||||||
|
R2xEZndOU3Y3eGxjbjZBPT0sdHlwZTpzdHJdIiwKCQkicGdwIjogbnVsbCwKCQkidW5lbm
|
||||||
|
NyeXB0ZWRfc3VmZml4IjogIl91bmVuY3J5cHRlZCIsCgkJInZlcnNpb24iOiAiMy43LjEi
|
||||||
|
Cgl9Cn0=
|
||||||
|
`,
|
||||||
|
expected: `apiVersion: v1
|
||||||
|
data:
|
||||||
|
token: KipTT1BTKio=
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: my-secret
|
||||||
|
type: Opaque
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "secret with basic auth",
|
||||||
|
yamlStr: `apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: secret-basic-auth
|
||||||
|
type: kubernetes.io/basic-auth
|
||||||
|
data:
|
||||||
|
username: admin
|
||||||
|
password: password
|
||||||
|
`,
|
||||||
|
expected: `apiVersion: v1
|
||||||
|
data:
|
||||||
|
password: password
|
||||||
|
username: admin
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: secret-basic-auth
|
||||||
|
type: kubernetes.io/basic-auth
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
r, err := yaml.Parse(tc.yamlStr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to parse yaml: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resource := &resource.Resource{RNode: *r}
|
||||||
|
err = trimSopsData(resource)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to trim sops data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sYaml, err := resource.AsYAML()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to convert sanitized resources to yaml: %v", err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(string(sYaml), tc.expected); diff != "" {
|
||||||
|
t.Errorf("unexpected sanitized resources: (-got +want)%v", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Flux 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 kustomization
|
||||||
|
|
||||||
|
import (
|
||||||
|
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
|
||||||
|
"github.com/fluxcd/pkg/apis/kustomize"
|
||||||
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Kustomize defines the methods to retrieve the kustomization information
|
||||||
|
// TO DO @souleb: move this to fluxcd/pkg along with generator and varsub
|
||||||
|
type Kustomize interface {
|
||||||
|
client.Object
|
||||||
|
GetTargetNamespace() string
|
||||||
|
GetPatches() []kustomize.Patch
|
||||||
|
GetPatchesStrategicMerge() []apiextensionsv1.JSON
|
||||||
|
GetPatchesJSON6902() []kustomize.JSON6902Patch
|
||||||
|
GetImages() []kustomize.Image
|
||||||
|
GetSubstituteFrom() []SubstituteReference
|
||||||
|
GetSubstitute() map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubstituteReference contains a reference to a resource containing
|
||||||
|
// the variables name and value.
|
||||||
|
type SubstituteReference struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TO DO @souleb: this is a temporary hack to get the kustomize object
|
||||||
|
// from the kustomize controller.
|
||||||
|
// At some point we should remove this and have the kustomize controller implement
|
||||||
|
// the Kustomize interface.
|
||||||
|
type kustomizeImpl struct {
|
||||||
|
kustomizev1.Kustomization
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kustomizeImpl) GetTargetNamespace() string {
|
||||||
|
return k.Spec.TargetNamespace
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kustomizeImpl) GetPatches() []kustomize.Patch {
|
||||||
|
return k.Spec.Patches
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kustomizeImpl) GetPatchesStrategicMerge() []apiextensionsv1.JSON {
|
||||||
|
return k.Spec.PatchesStrategicMerge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kustomizeImpl) GetPatchesJSON6902() []kustomize.JSON6902Patch {
|
||||||
|
return k.Spec.PatchesJSON6902
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kustomizeImpl) GetImages() []kustomize.Image {
|
||||||
|
return k.Spec.Images
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kustomizeImpl) GetSubstituteFrom() []SubstituteReference {
|
||||||
|
refs := make([]SubstituteReference, 0, len(k.Spec.PostBuild.SubstituteFrom))
|
||||||
|
for _, s := range k.Spec.PostBuild.SubstituteFrom {
|
||||||
|
refs = append(refs, SubstituteReference(s))
|
||||||
|
}
|
||||||
|
return refs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *kustomizeImpl) GetSubstitute() map[string]string {
|
||||||
|
return k.Spec.PostBuild.Substitute
|
||||||
|
}
|
@ -0,0 +1,258 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Flux 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 kustomization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"sigs.k8s.io/kustomize/api/konfig"
|
||||||
|
"sigs.k8s.io/kustomize/api/krusty"
|
||||||
|
"sigs.k8s.io/kustomize/api/provider"
|
||||||
|
"sigs.k8s.io/kustomize/api/resmap"
|
||||||
|
kustypes "sigs.k8s.io/kustomize/api/types"
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/filesys"
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
|
"github.com/fluxcd/pkg/apis/kustomize"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KustomizeGenerator struct {
|
||||||
|
kustomization Kustomize
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGenerator(kustomization Kustomize) *KustomizeGenerator {
|
||||||
|
return &KustomizeGenerator{
|
||||||
|
kustomization: kustomization,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFile generates a kustomization.yaml in the given directory if it does not exist.
|
||||||
|
// It apply the flux kustomize resources to the kustomization.yaml and then write the
|
||||||
|
// updated kustomization.yaml to the directory.
|
||||||
|
// It returns the original kustomization.yaml.
|
||||||
|
func (kg *KustomizeGenerator) WriteFile(dirPath string) ([]byte, error) {
|
||||||
|
if err := kg.generateKustomization(dirPath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName())
|
||||||
|
|
||||||
|
data, err := os.ReadFile(kfile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
kus := kustypes.Kustomization{
|
||||||
|
TypeMeta: kustypes.TypeMeta{
|
||||||
|
APIVersion: kustypes.KustomizationVersion,
|
||||||
|
Kind: kustypes.KustomizationKind,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(data, &kus); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if kg.kustomization.GetTargetNamespace() != "" {
|
||||||
|
kus.Namespace = kg.kustomization.GetTargetNamespace()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range kg.kustomization.GetPatches() {
|
||||||
|
kus.Patches = append(kus.Patches, kustypes.Patch{
|
||||||
|
Patch: m.Patch,
|
||||||
|
Target: adaptSelector(&m.Target),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range kg.kustomization.GetPatchesStrategicMerge() {
|
||||||
|
kus.PatchesStrategicMerge = append(kus.PatchesStrategicMerge, kustypes.PatchStrategicMerge(m.Raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range kg.kustomization.GetPatchesJSON6902() {
|
||||||
|
patch, err := json.Marshal(m.Patch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kus.PatchesJson6902 = append(kus.PatchesJson6902, kustypes.Patch{
|
||||||
|
Patch: string(patch),
|
||||||
|
Target: adaptSelector(&m.Target),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, image := range kg.kustomization.GetImages() {
|
||||||
|
newImage := kustypes.Image{
|
||||||
|
Name: image.Name,
|
||||||
|
NewName: image.NewName,
|
||||||
|
NewTag: image.NewTag,
|
||||||
|
}
|
||||||
|
if exists, index := checkKustomizeImageExists(kus.Images, image.Name); exists {
|
||||||
|
kus.Images[index] = newImage
|
||||||
|
} else {
|
||||||
|
kus.Images = append(kus.Images, newImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest, err := yaml.Marshal(kus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
os.WriteFile(kfile, manifest, 0644)
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkKustomizeImageExists(images []kustypes.Image, imageName string) (bool, int) {
|
||||||
|
for i, image := range images {
|
||||||
|
if imageName == image.Name {
|
||||||
|
return true, i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kg *KustomizeGenerator) generateKustomization(dirPath string) error {
|
||||||
|
fs := filesys.MakeFsOnDisk()
|
||||||
|
|
||||||
|
// Determine if there already is a Kustomization file at the root,
|
||||||
|
// as this means we do not have to generate one.
|
||||||
|
for _, kfilename := range konfig.RecognizedKustomizationFileNames() {
|
||||||
|
if kpath := filepath.Join(dirPath, kfilename); fs.Exists(kpath) && !fs.IsDir(kpath) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scan := func(base string) ([]string, error) {
|
||||||
|
var paths []string
|
||||||
|
pvd := provider.NewDefaultDepProvider()
|
||||||
|
rf := pvd.GetResourceFactory()
|
||||||
|
err := fs.Walk(base, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if path == base {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
// If a sub-directory contains an existing kustomization file add the
|
||||||
|
// directory as a resource and do not decend into it.
|
||||||
|
for _, kfilename := range konfig.RecognizedKustomizationFileNames() {
|
||||||
|
if kpath := filepath.Join(path, kfilename); fs.Exists(kpath) && !fs.IsDir(kpath) {
|
||||||
|
paths = append(paths, path)
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
extension := filepath.Ext(path)
|
||||||
|
if extension != ".yaml" && extension != ".yml" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fContents, err := fs.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := rf.SliceFromBytes(fContents); err != nil {
|
||||||
|
return fmt.Errorf("failed to decode Kubernetes YAML from %s: %w", path, err)
|
||||||
|
}
|
||||||
|
paths = append(paths, path)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return paths, err
|
||||||
|
}
|
||||||
|
|
||||||
|
abs, err := filepath.Abs(dirPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := scan(abs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName())
|
||||||
|
f, err := fs.Create(kfile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
kus := kustypes.Kustomization{
|
||||||
|
TypeMeta: kustypes.TypeMeta{
|
||||||
|
APIVersion: kustypes.KustomizationVersion,
|
||||||
|
Kind: kustypes.KustomizationKind,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var resources []string
|
||||||
|
for _, file := range files {
|
||||||
|
resources = append(resources, strings.Replace(file, abs, ".", 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
kus.Resources = resources
|
||||||
|
kd, err := yaml.Marshal(kus)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(kfile, kd, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func adaptSelector(selector *kustomize.Selector) (output *kustypes.Selector) {
|
||||||
|
if selector != nil {
|
||||||
|
output = &kustypes.Selector{}
|
||||||
|
output.Gvk.Group = selector.Group
|
||||||
|
output.Gvk.Kind = selector.Kind
|
||||||
|
output.Gvk.Version = selector.Version
|
||||||
|
output.Name = selector.Name
|
||||||
|
output.Namespace = selector.Namespace
|
||||||
|
output.LabelSelector = selector.LabelSelector
|
||||||
|
output.AnnotationSelector = selector.AnnotationSelector
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove mutex when kustomize fixes the concurrent map read/write panic
|
||||||
|
var kustomizeBuildMutex sync.Mutex
|
||||||
|
|
||||||
|
// buildKustomization wraps krusty.MakeKustomizer with the following settings:
|
||||||
|
// - load files from outside the kustomization.yaml root
|
||||||
|
// - disable plugins except for the builtin ones
|
||||||
|
func buildKustomization(fs filesys.FileSystem, dirPath string) (resmap.ResMap, error) {
|
||||||
|
// temporary workaround for concurrent map read and map write bug
|
||||||
|
// https://github.com/kubernetes-sigs/kustomize/issues/3659
|
||||||
|
kustomizeBuildMutex.Lock()
|
||||||
|
defer kustomizeBuildMutex.Unlock()
|
||||||
|
|
||||||
|
buildOptions := &krusty.Options{
|
||||||
|
LoadRestrictions: kustypes.LoadRestrictionsNone,
|
||||||
|
PluginConfig: kustypes.DisabledPluginConfig(),
|
||||||
|
}
|
||||||
|
|
||||||
|
k := krusty.MakeKustomizer(buildOptions)
|
||||||
|
return k.Run(fs, dirPath)
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Flux 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 kustomization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/drone/envsubst"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/kustomize/api/resource"
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// varsubRegex is the regular expression used to validate
|
||||||
|
// the var names before substitution
|
||||||
|
varsubRegex = "^[_[:alpha:]][_[:alpha:][:digit:]]*$"
|
||||||
|
DisabledValue = "disabled"
|
||||||
|
)
|
||||||
|
|
||||||
|
// substituteVariables replaces the vars with their values in the specified resource.
|
||||||
|
// If a resource is labeled or annotated with
|
||||||
|
// 'kustomize.toolkit.fluxcd.io/substitute: disabled' the substitution is skipped.
|
||||||
|
func substituteVariables(
|
||||||
|
ctx context.Context,
|
||||||
|
kubeClient client.Client,
|
||||||
|
kustomization Kustomize,
|
||||||
|
res *resource.Resource) (*resource.Resource, error) {
|
||||||
|
resData, err := res.AsYAML()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
key := fmt.Sprintf("%s/substitute", kustomization.GetObjectKind().GroupVersionKind().Group)
|
||||||
|
|
||||||
|
if res.GetLabels()[key] == DisabledValue || res.GetAnnotations()[key] == DisabledValue {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := make(map[string]string)
|
||||||
|
|
||||||
|
// load vars from ConfigMaps and Secrets data keys
|
||||||
|
for _, reference := range kustomization.GetSubstituteFrom() {
|
||||||
|
namespacedName := types.NamespacedName{Namespace: kustomization.GetNamespace(), Name: reference.Name}
|
||||||
|
switch reference.Kind {
|
||||||
|
case "ConfigMap":
|
||||||
|
resource := &corev1.ConfigMap{}
|
||||||
|
if err := kubeClient.Get(ctx, namespacedName, resource); err != nil {
|
||||||
|
return nil, fmt.Errorf("substitute from 'ConfigMap/%s' error: %w", reference.Name, err)
|
||||||
|
}
|
||||||
|
for k, v := range resource.Data {
|
||||||
|
vars[k] = strings.Replace(v, "\n", "", -1)
|
||||||
|
}
|
||||||
|
case "Secret":
|
||||||
|
resource := &corev1.Secret{}
|
||||||
|
if err := kubeClient.Get(ctx, namespacedName, resource); err != nil {
|
||||||
|
return nil, fmt.Errorf("substitute from 'Secret/%s' error: %w", reference.Name, err)
|
||||||
|
}
|
||||||
|
for k, v := range resource.Data {
|
||||||
|
vars[k] = strings.Replace(string(v), "\n", "", -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load in-line vars (overrides the ones from resources)
|
||||||
|
if kustomization.GetSubstitute() != nil {
|
||||||
|
for k, v := range kustomization.GetSubstitute() {
|
||||||
|
vars[k] = strings.Replace(v, "\n", "", -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run bash variable substitutions
|
||||||
|
if len(vars) > 0 {
|
||||||
|
r, _ := regexp.Compile(varsubRegex)
|
||||||
|
for v := range vars {
|
||||||
|
if !r.MatchString(v) {
|
||||||
|
return nil, fmt.Errorf("'%s' var name is invalid, must match '%s'", v, varsubRegex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := envsubst.Eval(string(resData), func(s string) string {
|
||||||
|
return vars[s]
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("variable substitution failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := yaml.YAMLToJSON([]byte(output))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("YAMLToJSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = res.UnmarshalJSON(jsonData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("UnmarshalJSON: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
Loading…
Reference in New Issue