Add graceful shutdown when interrupted

If implemented this permit restoring a clean state in case of signal
interruption.

Signed-off-by: Soule BA <soule@weave.works>
pull/2167/head
Soule BA 3 years ago
parent f7d9ee90cd
commit 306f8f5715
No known key found for this signature in database
GPG Key ID: 4D40965192802994

@ -19,10 +19,11 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"os/signal"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/fluxcd/flux2/internal/kustomization" "github.com/fluxcd/flux2/internal/build"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
) )
@ -33,7 +34,7 @@ var buildKsCmd = &cobra.Command{
Long: `The build command queries the Kubernetes API and fetches the specified Flux Kustomization. Long: `The build command queries the Kubernetes API and fetches the specified Flux Kustomization.
It then uses the fetched in cluster flux kustomization to perform needed transformation on the local kustomization.yaml It then uses the fetched in cluster flux kustomization to perform needed transformation on the local kustomization.yaml
pointed at by --path. The local kustomization.yaml is generated if it does not exist. Finally it builds the overlays using the local kustomization.yaml, and write the resulting multi-doc YAML to stdout.`, pointed at by --path. The local kustomization.yaml is generated if it does not exist. Finally it builds the overlays using the local kustomization.yaml, and write the resulting multi-doc YAML to stdout.`,
Example: `# Create a new overlay. Example: `# Build the local manifests as they were built on the cluster
flux build kustomization my-app --path ./path/to/local/manifests`, flux build kustomization my-app --path ./path/to/local/manifests`,
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
RunE: buildKsCmdRun, RunE: buildKsCmdRun,
@ -64,18 +65,36 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid resource path %q", buildKsArgs.path) return fmt.Errorf("invalid resource path %q", buildKsArgs.path)
} }
builder, err := kustomization.NewBuilder(kubeconfigArgs, name, buildKsArgs.path, kustomization.WithTimeout(rootArgs.timeout)) builder, err := build.NewBuilder(kubeconfigArgs, name, buildKsArgs.path, build.WithTimeout(rootArgs.timeout))
if err != nil { if err != nil {
return err return err
} }
manifests, err := builder.Build() // create a signal channel
if err != nil { sigc := make(chan os.Signal, 1)
return err signal.Notify(sigc, os.Interrupt)
errChan := make(chan error)
go func() {
manifests, err := builder.Build()
if err != nil {
errChan <- err
}
cmd.Print(string(manifests))
errChan <- nil
}()
select {
case <-sigc:
fmt.Println("Build cancelled... exiting.")
return builder.Cancel()
case err := <-errChan:
if err != nil {
return err
}
} }
cmd.Print(string(manifests))
return nil return nil
} }

@ -19,10 +19,11 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"os/signal"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/fluxcd/flux2/internal/kustomization" "github.com/fluxcd/flux2/internal/build"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
) )
@ -31,7 +32,7 @@ var diffKsCmd = &cobra.Command{
Aliases: []string{"ks"}, Aliases: []string{"ks"},
Short: "Diff Kustomization", Short: "Diff Kustomization",
Long: `The diff command does a build, then it performs a server-side dry-run and output the diff.`, Long: `The diff command does a build, then it performs a server-side dry-run and output the diff.`,
Example: `# Create a new overlay. Example: `# Preview changes local changes as they were applied on the cluster
flux diff kustomization my-app --path ./path/to/local/manifests`, flux diff kustomization my-app --path ./path/to/local/manifests`,
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
RunE: diffKsCmdRun, RunE: diffKsCmdRun,
@ -44,7 +45,7 @@ type diffKsFlags struct {
var diffKsArgs diffKsFlags var diffKsArgs diffKsFlags
func init() { func init() {
diffKsCmd.Flags().StringVar(&diffKsArgs.path, "path", "", "Name of a file containing a file to add to the kustomization file.)") diffKsCmd.Flags().StringVar(&diffKsArgs.path, "path", "", "Path to a local directory that matches the specified Kustomization.spec.path.)")
diffCmd.AddCommand(diffKsCmd) diffCmd.AddCommand(diffKsCmd)
} }
@ -62,18 +63,36 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid resource path %q", diffKsArgs.path) return fmt.Errorf("invalid resource path %q", diffKsArgs.path)
} }
builder, err := kustomization.NewBuilder(kubeconfigArgs, name, diffKsArgs.path, kustomization.WithTimeout(rootArgs.timeout)) builder, err := build.NewBuilder(kubeconfigArgs, name, diffKsArgs.path, build.WithTimeout(rootArgs.timeout))
if err != nil { if err != nil {
return err return err
} }
output, err := builder.Diff() // create a signal channel
if err != nil { sigc := make(chan os.Signal, 1)
return err signal.Notify(sigc, os.Interrupt)
errChan := make(chan error)
go func() {
output, err := builder.Diff()
if err != nil {
errChan <- err
}
cmd.Print(output)
errChan <- nil
}()
select {
case <-sigc:
fmt.Println("Build cancelled... exiting.")
return builder.Cancel()
case err := <-errChan:
if err != nil {
return err
}
} }
cmd.Print(output)
return nil return nil
} }

@ -25,7 +25,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/fluxcd/flux2/internal/kustomization" "github.com/fluxcd/flux2/internal/build"
"github.com/fluxcd/pkg/ssa" "github.com/fluxcd/pkg/ssa"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
) )
@ -85,7 +85,7 @@ func TestDiffKustomization(t *testing.T) {
"fluxns": allocateNamespace("flux-system"), "fluxns": allocateNamespace("flux-system"),
} }
b, _ := kustomization.NewBuilder(kubeconfigArgs, "podinfo", "") b, _ := build.NewBuilder(kubeconfigArgs, "podinfo", "")
resourceManager, err := b.Manager() resourceManager, err := b.Manager()
if err != nil { if err != nil {

@ -71,4 +71,4 @@ spec:
memory: 512Mi memory: 512Mi
requests: requests:
cpu: 100m cpu: 100m
memory: 64Mi memory: 64Mi

@ -17,4 +17,4 @@ spec:
type: Utilization type: Utilization
# scale up if usage is above # scale up if usage is above
# 99% of the requested CPU (100m) # 99% of the requested CPU (100m)
averageUtilization: 99 averageUtilization: 99

@ -12,4 +12,4 @@ spec:
sourceRef: sourceRef:
kind: GitRepository kind: GitRepository
name: podinfo name: podinfo
targetNamespace: default targetNamespace: default

@ -13,4 +13,4 @@ spec:
interval: 30s interval: 30s
ref: ref:
branch: master branch: master
url: https://github.com/stefanprodan/podinfo url: https://github.com/stefanprodan/podinfo

@ -71,4 +71,4 @@ spec:
memory: 512Mi memory: 512Mi
requests: requests:
cpu: 100m cpu: 100m
memory: 64Mi memory: 64Mi

@ -17,4 +17,4 @@ spec:
type: Utilization type: Utilization
# scale up if usage is above # scale up if usage is above
# 99% of the requested CPU (100m) # 99% of the requested CPU (100m)
averageUtilization: 99 averageUtilization: 99

@ -14,4 +14,4 @@ spec:
- port: 9999 - port: 9999
targetPort: grpc targetPort: grpc
protocol: TCP protocol: TCP
name: grpc name: grpc

@ -75,4 +75,4 @@ spec:
memory: 512Mi memory: 512Mi
requests: requests:
cpu: 100m cpu: 100m
memory: 64Mi memory: 64Mi

@ -8,4 +8,4 @@ secretGenerator:
- literals: - literals:
- username=admin - username=admin
- password=1f2d1e2e67df - password=1f2d1e2e67df
name: secret-basic-auth name: secret-basic-auth

@ -9,4 +9,4 @@ metadata:
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: db-user-pass-bkbd782d2c name: db-user-pass-bkbd782d2c
namespace: default namespace: default
type: Opaque type: Opaque

@ -18,4 +18,4 @@ spec:
- port: 9999 - port: 9999
targetPort: grpc targetPort: grpc
protocol: TCP protocol: TCP
name: grpc name: grpc

@ -8,4 +8,4 @@ metadata:
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: podinfo-token-77t89m9b67 name: podinfo-token-77t89m9b67
namespace: default namespace: default
type: Opaque type: Opaque

@ -12,10 +12,10 @@ require (
github.com/fluxcd/image-reflector-controller/api v0.15.0 github.com/fluxcd/image-reflector-controller/api v0.15.0
github.com/fluxcd/kustomize-controller/api v0.19.1 github.com/fluxcd/kustomize-controller/api v0.19.1
github.com/fluxcd/notification-controller/api v0.20.1 github.com/fluxcd/notification-controller/api v0.20.1
github.com/fluxcd/pkg/apis/kustomize v0.3.1 github.com/fluxcd/pkg/apis/kustomize v0.3.1 // indirect
github.com/fluxcd/pkg/apis/meta v0.10.2 github.com/fluxcd/pkg/apis/meta v0.10.2
github.com/fluxcd/pkg/runtime v0.12.3 github.com/fluxcd/pkg/runtime v0.12.3
github.com/fluxcd/pkg/ssa v0.10.0 github.com/fluxcd/pkg/ssa v0.11.0
github.com/fluxcd/pkg/ssh v0.3.1 github.com/fluxcd/pkg/ssh v0.3.1
github.com/fluxcd/pkg/untar v0.0.5 github.com/fluxcd/pkg/untar v0.0.5
github.com/fluxcd/pkg/version v0.0.1 github.com/fluxcd/pkg/version v0.0.1
@ -25,7 +25,7 @@ require (
github.com/gonvenience/ytbx v1.4.2 github.com/gonvenience/ytbx v1.4.2
github.com/google/go-cmp v0.5.6 github.com/google/go-cmp v0.5.6
github.com/google/go-containerregistry v0.2.0 github.com/google/go-containerregistry v0.2.0
github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
github.com/homeport/dyff v1.4.6 github.com/homeport/dyff v1.4.6
github.com/lucasb-eyer/go-colorful v1.2.0 github.com/lucasb-eyer/go-colorful v1.2.0
@ -49,7 +49,7 @@ require (
) )
require ( require (
github.com/drone/envsubst v1.0.3 github.com/fluxcd/pkg/kustomize v0.0.2
sigs.k8s.io/kustomize/kyaml v0.13.0 sigs.k8s.io/kustomize/kyaml v0.13.0
) )
@ -72,6 +72,7 @@ require (
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 // indirect
github.com/emirpasic/gods v1.12.0 // indirect github.com/emirpasic/gods v1.12.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect

@ -194,8 +194,8 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 h1:7QPwrLT79GlD5sizHf27aoY2RTvw62mO6x7mxkScNk0=
github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46/go.mod h1:esf2rsHFNlZlxsqsZDojNBcnNs5REqIvRrWRHqX0vEU=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=
@ -240,10 +240,12 @@ github.com/fluxcd/pkg/apis/kustomize v0.3.1 h1:wmb5D9e1+Rr3/5O3235ERuj+h2VKUArVf
github.com/fluxcd/pkg/apis/kustomize v0.3.1/go.mod h1:k2HSRd68UwgNmOYBPOd6WbX6a2MH2X/Jeh7e3s3PFPc= github.com/fluxcd/pkg/apis/kustomize v0.3.1/go.mod h1:k2HSRd68UwgNmOYBPOd6WbX6a2MH2X/Jeh7e3s3PFPc=
github.com/fluxcd/pkg/apis/meta v0.10.2 h1:pnDBBEvfs4HaKiVAYgz+e/AQ8dLvcgmVfSeBroZ/KKI= github.com/fluxcd/pkg/apis/meta v0.10.2 h1:pnDBBEvfs4HaKiVAYgz+e/AQ8dLvcgmVfSeBroZ/KKI=
github.com/fluxcd/pkg/apis/meta v0.10.2/go.mod h1:KQ2er9xa6koy7uoPMZjIjNudB5p4tXs+w0GO6fRcy7I= github.com/fluxcd/pkg/apis/meta v0.10.2/go.mod h1:KQ2er9xa6koy7uoPMZjIjNudB5p4tXs+w0GO6fRcy7I=
github.com/fluxcd/pkg/kustomize v0.0.2 h1:ipvQrxSeuGZDsPZrVUL6tYMlTR5xqYTZp6G0Tdy2hVs=
github.com/fluxcd/pkg/kustomize v0.0.2/go.mod h1:AFwnf3OqQmpTCuwCARTGpPRMBf0ZFJNGCvW63KbgK04=
github.com/fluxcd/pkg/runtime v0.12.3 h1:h21AZ3YG5MAP7DxFF9hfKrP+vFzys2L7CkUbPFjbP/0= github.com/fluxcd/pkg/runtime v0.12.3 h1:h21AZ3YG5MAP7DxFF9hfKrP+vFzys2L7CkUbPFjbP/0=
github.com/fluxcd/pkg/runtime v0.12.3/go.mod h1:imJ2xYy/d4PbSinX2IefmZk+iS2c1P5fY0js8mCE4SM= github.com/fluxcd/pkg/runtime v0.12.3/go.mod h1:imJ2xYy/d4PbSinX2IefmZk+iS2c1P5fY0js8mCE4SM=
github.com/fluxcd/pkg/ssa v0.10.0 h1:dhgWDeqz0/zAs5guzmPx/DMPCkzZdlEiPvCs1NChAQM= github.com/fluxcd/pkg/ssa v0.11.0 h1:ejEMlHPsbXMP8BJQx3+0PwoBgJur0mHiPcMNKcFwoOE=
github.com/fluxcd/pkg/ssa v0.10.0/go.mod h1:S+qig7BTOxop0c134y8Yv8/iQST4Kt7S2xXiFkP4VMA= github.com/fluxcd/pkg/ssa v0.11.0/go.mod h1:S+qig7BTOxop0c134y8Yv8/iQST4Kt7S2xXiFkP4VMA=
github.com/fluxcd/pkg/ssh v0.3.1 h1:iQw07bkX2rScactX8WYv+uMDsufFOlg8M3fV2TNs244= github.com/fluxcd/pkg/ssh v0.3.1 h1:iQw07bkX2rScactX8WYv+uMDsufFOlg8M3fV2TNs244=
github.com/fluxcd/pkg/ssh v0.3.1/go.mod h1:Sebfv4Um51PvomuYdMvKRncQW5dtKhZ5J5TA+wiHNSQ= github.com/fluxcd/pkg/ssh v0.3.1/go.mod h1:Sebfv4Um51PvomuYdMvKRncQW5dtKhZ5J5TA+wiHNSQ=
github.com/fluxcd/pkg/untar v0.0.5 h1:UGI3Ch1UIEIaqQvMicmImL1s9npQa64DJ/ozqHKB7gk= github.com/fluxcd/pkg/untar v0.0.5 h1:UGI3Ch1UIEIaqQvMicmImL1s9npQa64DJ/ozqHKB7gk=

@ -14,17 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package kustomization package build
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"sync"
"time" "time"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/internal/utils"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
"github.com/fluxcd/pkg/kustomize"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -37,9 +39,9 @@ import (
) )
const ( const (
controllerName = "kustomize-controller" controllerName = "kustomize-controller"
controllerGroup = "kustomize.toolkit.fluxcd.io" controllerGroup = "kustomize.toolkit.fluxcd.io"
mask string = "**SOPS**" mask = "**SOPS**"
) )
var defaultTimeout = 80 * time.Second var defaultTimeout = 80 * time.Second
@ -53,6 +55,9 @@ type Builder struct {
name string name string
namespace string namespace string
resourcesPath string resourcesPath string
// mu is used to synchronize access to the kustomization file
mu sync.Mutex
action kustomize.Action
kustomization *kustomizev1.Kustomization kustomization *kustomizev1.Kustomization
timeout time.Duration timeout time.Duration
} }
@ -149,13 +154,15 @@ func (b *Builder) build() (m resmap.ResMap, err error) {
// generate kustomization.yaml if needed // generate kustomization.yaml if needed
action, er := b.generate(*k, b.resourcesPath) action, er := b.generate(*k, b.resourcesPath)
if er != nil { if er != nil {
errf := CleanDirectory(b.resourcesPath, action) errf := kustomize.CleanDirectory(b.resourcesPath, action)
err = fmt.Errorf("failed to generate kustomization.yaml: %w", fmt.Errorf("%v %v", er, errf)) err = fmt.Errorf("failed to generate kustomization.yaml: %w", fmt.Errorf("%v %v", er, errf))
return return
} }
b.action = action
defer func() { defer func() {
errf := CleanDirectory(b.resourcesPath, action) errf := b.Cancel()
if err == nil { if err == nil {
err = errf err = errf
} }
@ -185,18 +192,28 @@ func (b *Builder) build() (m resmap.ResMap, err error) {
} }
func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath string) (action, error) { func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath string) (kustomize.Action, error) {
data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&kustomization) data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&kustomization)
if err != nil { if err != nil {
return "", err return "", err
} }
gen := NewGenerator(unstructured.Unstructured{Object: data}) gen := kustomize.NewGenerator(unstructured.Unstructured{Object: data})
return gen.WriteFile(dirPath, WithSaveOriginalKustomization())
// acuire the lock
b.mu.Lock()
defer b.mu.Unlock()
return gen.WriteFile(dirPath, kustomize.WithSaveOriginalKustomization())
} }
func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomization, dirPath string) (resmap.ResMap, error) { func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomization, dirPath string) (resmap.ResMap, error) {
fs := filesys.MakeFsOnDisk() fs := filesys.MakeFsOnDisk()
m, err := BuildKustomization(fs, dirPath)
// acuire the lock
b.mu.Lock()
defer b.mu.Unlock()
m, err := kustomize.BuildKustomization(fs, dirPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("kustomize build failed: %w", err) return nil, fmt.Errorf("kustomize build failed: %w", err)
} }
@ -208,7 +225,7 @@ func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomizatio
if err != nil { if err != nil {
return nil, err return nil, err
} }
outRes, err := SubstituteVariables(ctx, b.client, unstructured.Unstructured{Object: data}, res) outRes, err := kustomize.SubstituteVariables(ctx, b.client, unstructured.Unstructured{Object: data}, res)
if err != nil { if err != nil {
return nil, fmt.Errorf("var substitution failed for '%s': %w", res.GetName(), err) return nil, fmt.Errorf("var substitution failed for '%s': %w", res.GetName(), err)
} }
@ -263,3 +280,18 @@ func trimSopsData(res *resource.Resource) error {
return nil return nil
} }
// Cancel cancels the build
// It restores a clean reprository
func (b *Builder) Cancel() error {
// acuire the lock
b.mu.Lock()
defer b.mu.Unlock()
err := kustomize.CleanDirectory(b.resourcesPath, b.action)
if err != nil {
return err
}
return nil
}

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package kustomization package build
import ( import (
"testing" "testing"

@ -1,4 +1,4 @@
package kustomization package build
import ( import (
"bytes" "bytes"
@ -18,7 +18,6 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/homeport/dyff/pkg/dyff" "github.com/homeport/dyff/pkg/dyff"
"github.com/lucasb-eyer/go-colorful" "github.com/lucasb-eyer/go-colorful"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling" "sigs.k8s.io/cli-utils/pkg/kstatus/polling"
@ -27,7 +26,7 @@ import (
) )
func (b *Builder) Manager() (*ssa.ResourceManager, error) { func (b *Builder) Manager() (*ssa.ResourceManager, error) {
statusPoller := polling.NewStatusPoller(b.client, b.restMapper) statusPoller := polling.NewStatusPoller(b.client, b.restMapper, nil)
owner := ssa.Owner{ owner := ssa.Owner{
Field: controllerName, Field: controllerName,
Group: controllerGroup, Group: controllerGroup,
@ -53,8 +52,6 @@ func (b *Builder) Diff() (string, error) {
return "", err return "", err
} }
resourceManager.SetOwnerLabels(objects, b.kustomization.GetName(), b.kustomization.GetNamespace())
ctx, cancel := context.WithTimeout(context.Background(), b.timeout) ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel() defer cancel()
@ -65,12 +62,17 @@ func (b *Builder) Diff() (string, error) {
// create an inventory of objects to be reconciled // create an inventory of objects to be reconciled
newInventory := newInventory() newInventory := newInventory()
for _, obj := range objects { for _, obj := range objects {
change, liveObject, mergedObject, err := resourceManager.Diff(ctx, obj) diffOptions := ssa.DiffOptions{
Exclusions: map[string]string{
"kustomize.toolkit.fluxcd.io/reconcile": "disabled",
},
}
change, liveObject, mergedObject, err := resourceManager.Diff(ctx, obj, diffOptions)
if err != nil { if err != nil {
if b.kustomization.Spec.Force && isImmutableError(err) { if b.kustomization.Spec.Force && ssa.IsImmutableError(err) {
output.WriteString(writeString(fmt.Sprintf("► %s created\n", obj.GetName()), bunt.Green)) output.WriteString(writeString(fmt.Sprintf("► %s created\n", obj.GetName()), bunt.Green))
} else { } else {
output.WriteString(writeString(fmt.Sprint(``, err), bunt.Red)) output.WriteString(writeString(fmt.Sprintf("✗ %v\n", err), bunt.Red))
} }
continue continue
} }
@ -283,12 +285,3 @@ func addObjectsToInventory(inv *kustomizev1.ResourceInventory, entry *ssa.Change
return nil return nil
} }
func isImmutableError(err error) bool {
// Detect immutability like kubectl does
// https://github.com/kubernetes/kubectl/blob/8165f83007/pkg/cmd/apply/patcher.go#L201
if errors.IsConflict(err) || errors.IsInvalid(err) {
return true
}
return false
}

@ -1,24 +0,0 @@
/*
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
// SubstituteReference contains a reference to a resource containing
// the variables name and value.
type SubstituteReference struct {
Kind string `json:"kind"`
Name string `json:"name"`
}

@ -1,532 +0,0 @@
/*
Copyright 2022 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"
"io"
"os"
"path/filepath"
"strings"
"sync"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"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"
"github.com/hashicorp/go-multierror"
)
const (
specField = "spec"
targetNSField = "targetNamespace"
patchesField = "patches"
patchesSMField = "patchesStrategicMerge"
patchesJson6902Field = "patchesJson6902"
imagesField = "images"
originalKustomizationFile = "kustomization.yaml.original"
)
type action string
const (
createdAction action = "created"
unchangedAction action = "unchanged"
)
type KustomizeGenerator struct {
kustomization unstructured.Unstructured
}
type SavingOptions func(dirPath, file string, action action) error
func NewGenerator(kustomization unstructured.Unstructured) *KustomizeGenerator {
return &KustomizeGenerator{
kustomization: kustomization,
}
}
func WithSaveOriginalKustomization() SavingOptions {
return func(dirPath, kfile string, action action) error {
// copy the original kustomization.yaml to the directory if we did not create it
if action != createdAction {
if err := copyFile(kfile, filepath.Join(dirPath, originalKustomizationFile)); err != nil {
errf := CleanDirectory(dirPath, action)
return fmt.Errorf("%v %v", err, errf)
}
}
return nil
}
}
// 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 an action that indicates if the kustomization.yaml was created or not.
// It is the caller responsability to clean up the directory by use the provided function CleanDirectory.
// example:
// err := CleanDirectory(dirPath, action)
// if err != nil {
// log.Fatal(err)
// }
func (kg *KustomizeGenerator) WriteFile(dirPath string, opts ...SavingOptions) (action, error) {
action, err := kg.generateKustomization(dirPath)
if err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("%v %v", err, errf)
}
kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName())
data, err := os.ReadFile(kfile)
if err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("%w %s", err, errf)
}
kus := kustypes.Kustomization{
TypeMeta: kustypes.TypeMeta{
APIVersion: kustypes.KustomizationVersion,
Kind: kustypes.KustomizationKind,
},
}
if err := yaml.Unmarshal(data, &kus); err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("%v %v", err, errf)
}
tg, ok, err := kg.getNestedString(specField, targetNSField)
if err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("%v %v", err, errf)
}
if ok {
kus.Namespace = tg
}
patches, err := kg.getPatches()
if err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("unable to get patches: %w", fmt.Errorf("%v %v", err, errf))
}
for _, p := range patches {
kus.Patches = append(kus.Patches, kustypes.Patch{
Patch: p.Patch,
Target: adaptSelector(&p.Target),
})
}
patchesSM, err := kg.getPatchesStrategicMerge()
if err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("unable to get patchesStrategicMerge: %w", fmt.Errorf("%v %v", err, errf))
}
for _, p := range patchesSM {
kus.PatchesStrategicMerge = append(kus.PatchesStrategicMerge, kustypes.PatchStrategicMerge(p.Raw))
}
patchesJSON, err := kg.getPatchesJson6902()
if err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("unable to get patchesJson6902: %w", fmt.Errorf("%v %v", err, errf))
}
for _, p := range patchesJSON {
patch, err := json.Marshal(p.Patch)
if err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("%v %v", err, errf)
}
kus.PatchesJson6902 = append(kus.PatchesJson6902, kustypes.Patch{
Patch: string(patch),
Target: adaptSelector(&p.Target),
})
}
images, err := kg.getImages()
if err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("unable to get images: %w", fmt.Errorf("%v %v", err, errf))
}
for _, image := range images {
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 {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("%v %v", err, errf)
}
// copy the original kustomization.yaml to the directory if we did not create it
for _, opt := range opts {
if err := opt(dirPath, kfile, action); err != nil {
return action, fmt.Errorf("failed to save original kustomization.yaml: %w", err)
}
}
err = os.WriteFile(kfile, manifest, os.ModePerm)
if err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("%v %v", err, errf)
}
return action, nil
}
func (kg *KustomizeGenerator) getPatches() ([]kustomize.Patch, error) {
patches, ok, err := kg.getNestedSlice(specField, patchesField)
if err != nil {
return nil, err
}
var resultErr error
if ok {
res := make([]kustomize.Patch, 0, len(patches))
for k, p := range patches {
patch, ok := p.(map[string]interface{})
if !ok {
err := fmt.Errorf("unable to convert patch %d to map[string]interface{}", k)
resultErr = multierror.Append(resultErr, err)
}
var kpatch kustomize.Patch
err = runtime.DefaultUnstructuredConverter.FromUnstructured(patch, &kpatch)
if err != nil {
resultErr = multierror.Append(resultErr, err)
}
res = append(res, kpatch)
}
return res, resultErr
}
return nil, resultErr
}
func (kg *KustomizeGenerator) getPatchesStrategicMerge() ([]apiextensionsv1.JSON, error) {
patches, ok, err := kg.getNestedSlice(specField, patchesSMField)
if err != nil {
return nil, err
}
var resultErr error
if ok {
res := make([]apiextensionsv1.JSON, 0, len(patches))
for k, p := range patches {
patch, ok := p.(map[string]interface{})
if !ok {
err := fmt.Errorf("unable to convert patch %d to map[string]interface{}", k)
resultErr = multierror.Append(resultErr, err)
}
var kpatch apiextensionsv1.JSON
err = runtime.DefaultUnstructuredConverter.FromUnstructured(patch, &kpatch)
if err != nil {
resultErr = multierror.Append(resultErr, err)
}
res = append(res, kpatch)
}
return res, resultErr
}
return nil, resultErr
}
func (kg *KustomizeGenerator) getPatchesJson6902() ([]kustomize.JSON6902Patch, error) {
patches, ok, err := kg.getNestedSlice(specField, patchesJson6902Field)
if err != nil {
return nil, err
}
var resultErr error
if ok {
res := make([]kustomize.JSON6902Patch, 0, len(patches))
for k, p := range patches {
patch, ok := p.(map[string]interface{})
if !ok {
err := fmt.Errorf("unable to convert patch %d to map[string]interface{}", k)
resultErr = multierror.Append(resultErr, err)
}
var kpatch kustomize.JSON6902Patch
err = runtime.DefaultUnstructuredConverter.FromUnstructured(patch, &kpatch)
if err != nil {
resultErr = multierror.Append(resultErr, err)
}
res = append(res, kpatch)
}
return res, resultErr
}
return nil, resultErr
}
func (kg *KustomizeGenerator) getImages() ([]kustomize.Image, error) {
img, ok, err := kg.getNestedSlice(specField, imagesField)
if err != nil {
return nil, err
}
var resultErr error
if ok {
res := make([]kustomize.Image, 0, len(img))
for k, i := range img {
im, ok := i.(map[string]interface{})
if !ok {
err := fmt.Errorf("unable to convert patch %d to map[string]interface{}", k)
resultErr = multierror.Append(resultErr, err)
}
var image kustomize.Image
err = runtime.DefaultUnstructuredConverter.FromUnstructured(im, &image)
if err != nil {
resultErr = multierror.Append(resultErr, err)
}
res = append(res, image)
}
return res, resultErr
}
return nil, resultErr
}
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) getNestedString(fields ...string) (string, bool, error) {
val, ok, err := unstructured.NestedString(kg.kustomization.Object, fields...)
if err != nil {
return "", ok, err
}
return val, ok, nil
}
func (kg *KustomizeGenerator) getNestedSlice(fields ...string) ([]interface{}, bool, error) {
val, ok, err := unstructured.NestedSlice(kg.kustomization.Object, fields...)
if err != nil {
return nil, ok, err
}
return val, ok, nil
}
func (kg *KustomizeGenerator) generateKustomization(dirPath string) (action, 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 unchangedAction, 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 unchangedAction, err
}
files, err := scan(abs)
if err != nil {
return unchangedAction, err
}
kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName())
f, err := fs.Create(kfile)
if err != nil {
return unchangedAction, 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 {
// delete the kustomization file
errf := CleanDirectory(dirPath, createdAction)
return unchangedAction, fmt.Errorf("%v %v", err, errf)
}
return createdAction, 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)
}
// CleanDirectory removes the kustomization.yaml file from the given directory.
func CleanDirectory(dirPath string, action action) error {
kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName())
originalFile := filepath.Join(dirPath, originalKustomizationFile)
// restore old file if it exists
if _, err := os.Stat(originalFile); err == nil {
err := os.Rename(originalFile, kfile)
if err != nil {
return fmt.Errorf("failed to cleanup repository: %w", err)
}
}
if action == createdAction {
return os.Remove(kfile)
}
return nil
}
// copyFile copies the contents of the file named src to the file named
// by dst. The file will be created if it does not already exist or else trucnated.
func copyFile(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
errf := out.Close()
if err == nil {
err = errf
}
}()
if _, err = io.Copy(out, in); err != nil {
return
}
return
}

@ -1,176 +0,0 @@
/*
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"
"github.com/hashicorp/go-multierror"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"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 unstructured.Unstructured,
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
}
// load vars from ConfigMaps and Secrets data keys
vars, err := loadVars(ctx, kubeClient, kustomization)
if err != nil {
return nil, err
}
// load in-line vars (overrides the ones from resources)
substitute, ok, err := unstructured.NestedStringMap(kustomization.Object, "spec", "postBuild", "substitute")
if err != nil {
return nil, err
}
if ok {
for k, v := range substitute {
vars[k] = strings.Replace(v, "\n", "", -1)
}
}
// run bash variable substitutions
if len(vars) > 0 {
jsonData, err := varSubstitution(resData, vars)
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
}
func loadVars(ctx context.Context, kubeClient client.Client, kustomization unstructured.Unstructured) (map[string]string, error) {
vars := make(map[string]string)
substituteFrom, err := getSubstituteFrom(kustomization)
if err != nil {
return nil, fmt.Errorf("unable to get subsituteFrom: %w", err)
}
for _, reference := range substituteFrom {
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)
}
}
}
return vars, nil
}
func varSubstitution(data []byte, vars map[string]string) ([]byte, error) {
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(data), 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)
}
return jsonData, nil
}
func getSubstituteFrom(kustomization unstructured.Unstructured) ([]SubstituteReference, error) {
substituteFrom, ok, err := unstructured.NestedSlice(kustomization.Object, "spec", "postBuild", "substituteFrom")
if err != nil {
return nil, err
}
var resultErr error
if ok {
res := make([]SubstituteReference, 0, len(substituteFrom))
for k, s := range substituteFrom {
sub, ok := s.(map[string]interface{})
if !ok {
err := fmt.Errorf("unable to convert patch %d to map[string]interface{}", k)
resultErr = multierror.Append(resultErr, err)
}
var substitute SubstituteReference
err = runtime.DefaultUnstructuredConverter.FromUnstructured(sub, &substitute)
if err != nil {
resultErr = multierror.Append(resultErr, err)
}
res = append(res, substitute)
}
return res, nil
}
return nil, resultErr
}
Loading…
Cancel
Save