mirror of https://github.com/fluxcd/flux2.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
338 lines
8.7 KiB
Go
338 lines
8.7 KiB
Go
/*
|
|
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 build
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/gonvenience/bunt"
|
|
"github.com/gonvenience/ytbx"
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/homeport/dyff/pkg/dyff"
|
|
"github.com/lucasb-eyer/go-colorful"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/util/errors"
|
|
"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
|
|
"sigs.k8s.io/cli-utils/pkg/object"
|
|
"sigs.k8s.io/yaml"
|
|
|
|
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
|
"github.com/fluxcd/pkg/ssa"
|
|
|
|
"github.com/fluxcd/flux2/v2/pkg/printers"
|
|
)
|
|
|
|
func (b *Builder) Manager() (*ssa.ResourceManager, error) {
|
|
statusPoller := polling.NewStatusPoller(b.client, b.restMapper, polling.Options{})
|
|
owner := ssa.Owner{
|
|
Field: controllerName,
|
|
Group: controllerGroup,
|
|
}
|
|
|
|
return ssa.NewResourceManager(b.client, statusPoller, owner), nil
|
|
}
|
|
|
|
func (b *Builder) Diff() (string, bool, error) {
|
|
output := strings.Builder{}
|
|
createdOrDrifted := false
|
|
objects, err := b.Build()
|
|
if err != nil {
|
|
return "", createdOrDrifted, err
|
|
}
|
|
|
|
err = ssa.SetNativeKindsDefaults(objects)
|
|
if err != nil {
|
|
return "", createdOrDrifted, err
|
|
}
|
|
|
|
resourceManager, err := b.Manager()
|
|
if err != nil {
|
|
return "", createdOrDrifted, err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
|
|
defer cancel()
|
|
|
|
if b.spinner != nil {
|
|
err = b.spinner.Start()
|
|
if err != nil {
|
|
return "", false, fmt.Errorf("failed to start spinner: %w", err)
|
|
}
|
|
}
|
|
|
|
var diffErrs []error
|
|
// create an inventory of objects to be reconciled
|
|
newInventory := newInventory()
|
|
for _, obj := range objects {
|
|
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 {
|
|
// gather errors and continue, as we want to see all the diffs
|
|
diffErrs = append(diffErrs, err)
|
|
continue
|
|
}
|
|
|
|
// if the object is a sops secret, we need to
|
|
// make sure we diff only if the keys are different
|
|
if obj.GetKind() == "Secret" && change.Action == ssa.ConfiguredAction {
|
|
diffSopsSecret(obj, liveObject, mergedObject, change)
|
|
}
|
|
|
|
if change.Action == ssa.CreatedAction {
|
|
output.WriteString(writeString(fmt.Sprintf("► %s created\n", change.Subject), bunt.Green))
|
|
createdOrDrifted = true
|
|
}
|
|
|
|
if change.Action == ssa.ConfiguredAction {
|
|
output.WriteString(bunt.Sprint(fmt.Sprintf("► %s drifted\n", change.Subject)))
|
|
liveFile, mergedFile, tmpDir, err := writeYamls(liveObject, mergedObject)
|
|
if err != nil {
|
|
return "", createdOrDrifted, err
|
|
}
|
|
defer cleanupDir(tmpDir)
|
|
|
|
err = diff(liveFile, mergedFile, &output)
|
|
if err != nil {
|
|
return "", createdOrDrifted, err
|
|
}
|
|
|
|
createdOrDrifted = true
|
|
}
|
|
|
|
addObjectsToInventory(newInventory, change)
|
|
}
|
|
|
|
if b.spinner != nil {
|
|
b.spinner.Message("processing inventory")
|
|
}
|
|
|
|
if b.kustomization.Spec.Prune && len(diffErrs) == 0 {
|
|
oldStatus := b.kustomization.Status.DeepCopy()
|
|
if oldStatus.Inventory != nil {
|
|
staleObjects, err := diffInventory(oldStatus.Inventory, newInventory)
|
|
if err != nil {
|
|
return "", createdOrDrifted, err
|
|
}
|
|
if len(staleObjects) > 0 {
|
|
createdOrDrifted = true
|
|
}
|
|
for _, object := range staleObjects {
|
|
output.WriteString(writeString(fmt.Sprintf("► %s deleted\n", ssa.FmtUnstructured(object)), bunt.OrangeRed))
|
|
}
|
|
}
|
|
}
|
|
|
|
if b.spinner != nil {
|
|
err = b.spinner.Stop()
|
|
if err != nil {
|
|
return "", createdOrDrifted, fmt.Errorf("failed to stop spinner: %w", err)
|
|
}
|
|
}
|
|
|
|
return output.String(), createdOrDrifted, errors.Reduce(errors.Flatten(errors.NewAggregate(diffErrs)))
|
|
}
|
|
|
|
func writeYamls(liveObject, mergedObject *unstructured.Unstructured) (string, string, string, error) {
|
|
tmpDir, err := os.MkdirTemp("", "")
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
|
|
liveYAML, _ := yaml.Marshal(liveObject)
|
|
liveFile := filepath.Join(tmpDir, "live.yaml")
|
|
if err := os.WriteFile(liveFile, liveYAML, 0644); err != nil {
|
|
return "", "", "", err
|
|
}
|
|
|
|
mergedYAML, _ := yaml.Marshal(mergedObject)
|
|
mergedFile := filepath.Join(tmpDir, "merged.yaml")
|
|
if err := os.WriteFile(mergedFile, mergedYAML, 0644); err != nil {
|
|
return "", "", "", err
|
|
}
|
|
|
|
return liveFile, mergedFile, tmpDir, nil
|
|
}
|
|
|
|
func writeString(t string, color colorful.Color) string {
|
|
return bunt.Style(
|
|
t,
|
|
bunt.EachLine(),
|
|
bunt.Foreground(color),
|
|
)
|
|
}
|
|
|
|
func cleanupDir(dir string) error {
|
|
return os.RemoveAll(dir)
|
|
}
|
|
|
|
func diff(liveFile, mergedFile string, output io.Writer) error {
|
|
from, to, err := ytbx.LoadFiles(liveFile, mergedFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load input files: %w", err)
|
|
}
|
|
|
|
report, err := dyff.CompareInputFiles(from, to,
|
|
dyff.IgnoreOrderChanges(false),
|
|
dyff.KubernetesEntityDetection(true),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to compare input files: %w", err)
|
|
}
|
|
|
|
printer := printers.NewDyffPrinter()
|
|
|
|
printer.Print(output, report)
|
|
|
|
return nil
|
|
}
|
|
|
|
func diffSopsSecret(obj, liveObject, mergedObject *unstructured.Unstructured, change *ssa.ChangeSetEntry) {
|
|
// get both data and stringdata maps
|
|
data := obj.Object[dataField]
|
|
|
|
if m, ok := data.(map[string]interface{}); ok && m != nil {
|
|
applySopsDiff(m, liveObject, mergedObject, change)
|
|
}
|
|
}
|
|
|
|
func applySopsDiff(data map[string]interface{}, liveObject, mergedObject *unstructured.Unstructured, change *ssa.ChangeSetEntry) {
|
|
for _, v := range data {
|
|
v, err := base64.StdEncoding.DecodeString(v.(string))
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
|
|
if bytes.Contains(v, []byte(mask)) {
|
|
if liveObject != nil && mergedObject != nil {
|
|
change.Action = ssa.UnchangedAction
|
|
liveKeys, mergedKeys := sopsComparableByKeys(liveObject), sopsComparableByKeys(mergedObject)
|
|
if cmp.Diff(liveKeys, mergedKeys) != "" {
|
|
change.Action = ssa.ConfiguredAction
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func sopsComparableByKeys(object *unstructured.Unstructured) []string {
|
|
m := object.Object[dataField].(map[string]interface{})
|
|
keys := make([]string, len(m))
|
|
i := 0
|
|
for k := range m {
|
|
// make sure we can compare only on keys
|
|
m[k] = "*****"
|
|
keys[i] = k
|
|
i++
|
|
}
|
|
|
|
object.Object[dataField] = m
|
|
|
|
sort.Strings(keys)
|
|
|
|
return keys
|
|
}
|
|
|
|
// diffInventory returns the slice of objects that do not exist in the target inventory.
|
|
func diffInventory(inv *kustomizev1.ResourceInventory, target *kustomizev1.ResourceInventory) ([]*unstructured.Unstructured, error) {
|
|
versionOf := func(i *kustomizev1.ResourceInventory, objMetadata object.ObjMetadata) string {
|
|
for _, entry := range i.Entries {
|
|
if entry.ID == objMetadata.String() {
|
|
return entry.Version
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
objects := make([]*unstructured.Unstructured, 0)
|
|
aList, err := listMetaInInventory(inv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bList, err := listMetaInInventory(target)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
list := aList.Diff(bList)
|
|
if len(list) == 0 {
|
|
return objects, nil
|
|
}
|
|
|
|
for _, metadata := range list {
|
|
u := &unstructured.Unstructured{}
|
|
u.SetGroupVersionKind(schema.GroupVersionKind{
|
|
Group: metadata.GroupKind.Group,
|
|
Kind: metadata.GroupKind.Kind,
|
|
Version: versionOf(inv, metadata),
|
|
})
|
|
u.SetName(metadata.Name)
|
|
u.SetNamespace(metadata.Namespace)
|
|
objects = append(objects, u)
|
|
}
|
|
|
|
sort.Sort(ssa.SortableUnstructureds(objects))
|
|
return objects, nil
|
|
}
|
|
|
|
// listMetaInInventory returns the inventory entries as object.ObjMetadata objects.
|
|
func listMetaInInventory(inv *kustomizev1.ResourceInventory) (object.ObjMetadataSet, error) {
|
|
var metas []object.ObjMetadata
|
|
for _, e := range inv.Entries {
|
|
m, err := object.ParseObjMetadata(e.ID)
|
|
if err != nil {
|
|
return metas, err
|
|
}
|
|
metas = append(metas, m)
|
|
}
|
|
|
|
return metas, nil
|
|
}
|
|
|
|
func newInventory() *kustomizev1.ResourceInventory {
|
|
return &kustomizev1.ResourceInventory{
|
|
Entries: []kustomizev1.ResourceRef{},
|
|
}
|
|
}
|
|
|
|
// addObjectsToInventory extracts the metadata from the given objects and adds it to the inventory.
|
|
func addObjectsToInventory(inv *kustomizev1.ResourceInventory, entry *ssa.ChangeSetEntry) error {
|
|
if entry == nil {
|
|
return nil
|
|
}
|
|
|
|
inv.Entries = append(inv.Entries, kustomizev1.ResourceRef{
|
|
ID: entry.ObjMetadata.String(),
|
|
Version: entry.GroupVersion,
|
|
})
|
|
|
|
return nil
|
|
}
|