appconfigmgrv2/controllers/appenvconfigtemplatev2_controller.go (239 lines of code) (raw):
// Copyright 2019 Google LLC
//
// 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.
//
// Copyright 2019 Google LLC. This software is provided as-is,
// without warranty or representation for any use or purpose.
//
package controllers
import (
"context"
"fmt"
"reflect"
corev1 "k8s.io/api/core/v1"
"k8s.io/api/extensions/v1beta1"
netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/client-go/dynamic"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/source"
appconfig "github.com/GoogleCloudPlatform/anthos-appconfig/appconfigmgrv2/api/v1alpha1"
appconfigmgrv1alpha1 "github.com/GoogleCloudPlatform/anthos-appconfig/appconfigmgrv2/api/v1alpha1"
)
var log = ctrl.Log.WithName("controller")
// AppEnvConfigTemplateV2Reconciler reconciles a AppEnvConfigTemplateV2 object.
type AppEnvConfigTemplateV2Reconciler struct {
client.Client
Dynamic dynamic.Interface
Log logr.Logger
Scheme *runtime.Scheme
skipGatekeeper bool
}
// Reconcile takes an instance of an app config and issues create/update/delete requests
// to a number of resources. Behavior is dependant on whether or not istio auto-inject is
// enabled for the namespace.
func (r *AppEnvConfigTemplateV2Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
log = r.Log.WithValues("appenvconfigtemplatev2", req.NamespacedName)
log.Info("Starting reconcile")
defer log.Info("Reconcile complete")
// Relies on OPA Gatekeeper.
if !r.skipGatekeeper {
/* TODO: Check that app labels are valid via listing instances.
instanceList := &appconfigmgrv1alpha1.AppEnvConfigTemplateV2List{}
if err := r.List(ctx, instanceList); err != nil {
return ctrl.Result{}, err
}
*/
opaNamespaces, err := r.opaNamespaces(ctx)
if err != nil {
return ctrl.Result{}, fmt.Errorf("listing opa namespaces: %v", err)
}
if err := r.reconcileOPAContraints(ctx, opaNamespaces); err != nil {
return ctrl.Result{}, fmt.Errorf("reconciling opa constraints: %v", err)
}
}
instance := &appconfigmgrv1alpha1.AppEnvConfigTemplateV2{}
err := r.Get(ctx, req.NamespacedName, instance)
if err != nil {
if errors.IsNotFound(err) {
// Object not found, return. Created objects are automatically garbage collected.
// For additional cleanup logic use finalizers.
return ctrl.Result{}, nil
}
// Error reading the object - requeue the request.
return ctrl.Result{}, err
}
// If istio is enabled, we will light up certain features and use istio
// resources rather than native kubernetes resources for other features.
istioEnabled, err := r.istioAutoInjectEnabled(ctx, instance.Namespace)
if err != nil {
return ctrl.Result{}, fmt.Errorf("checking for istio auto-inject label: %v", err)
}
cfg, err := r.getConfig()
if err != nil {
return ctrl.Result{}, fmt.Errorf("getting config: %v", err)
}
log.Info("Reconciling", "resource", "services")
if err := r.reconcileServices(ctx, instance); err != nil {
return ctrl.Result{}, fmt.Errorf("reconciling services: %v", err)
}
log.Info("Reconciling", "resource", "ingress")
if err := r.reconcileIngress(ctx, instance); err != nil {
return ctrl.Result{}, fmt.Errorf("reconciling ingress: %v", err)
}
if istioEnabled {
log.Info("Reconciling", "resource", "virtualservices")
if err := r.reconcileIstioVirtualServices(ctx, instance); err != nil {
return ctrl.Result{}, fmt.Errorf("reconciling istio virtual services: %v", err)
}
log.Info("Reconciling", "resource", "policies")
if err := r.reconcileIstioPolicies(ctx, instance); err != nil {
return ctrl.Result{}, fmt.Errorf("reconciling istio policies: %v", err)
}
log.Info("Reconciling", "resource", "serviceentries")
if err := r.reconcileIstioServiceEntries(ctx, cfg, instance); err != nil {
return ctrl.Result{}, fmt.Errorf("reconciling istio service entries: %v", err)
}
log.Info("Reconciling", "resource", "instances")
if err := r.reconcileIstioInstances(ctx, instance); err != nil {
return ctrl.Result{}, fmt.Errorf("reconciling istio instances: %v", err)
}
log.Info("Reconciling", "resource", "handlers")
if err := r.reconcileIstioHandlers(ctx, cfg, instance); err != nil {
return ctrl.Result{}, fmt.Errorf("reconciling istio handlers: %v", err)
}
log.Info("Reconciling", "resource", "rules")
if err := r.reconcileIstioRules(ctx, cfg, instance); err != nil {
return ctrl.Result{}, fmt.Errorf("reconciling istio rules: %v", err)
}
} else {
log.Info("Reconciling", "resource", "networkpolicies")
if err := r.reconcileNetworkPolicies(ctx, instance); err != nil {
return ctrl.Result{}, fmt.Errorf("reconciling network policies: %v", err)
}
}
// TODO: Garbage collect istio/non-istio resources on namespace istio injection label update?
// i.e. NetworkPolicies vs istio Rules
vaultEnabled, err := r.vaultInjectEnabled(ctx, instance)
if err != nil {
return ctrl.Result{}, fmt.Errorf("checking vault gcpaccess config: %v", err)
}
if vaultEnabled {
if err := r.reconcileVault(ctx, instance); err != nil {
return ctrl.Result{}, fmt.Errorf("reconciling vault: %v", err)
}
}
return ctrl.Result{}, nil
}
// SetupWithManager registers the reconciler with a manager.
// The behavior is dependant on whether or not istio is installed.
// This is determined by the presence of istio CRDs.
func (r *AppEnvConfigTemplateV2Reconciler) SetupWithManager(mgr ctrl.Manager) error {
c := ctrl.NewControllerManagedBy(mgr).
For(&appconfigmgrv1alpha1.AppEnvConfigTemplateV2{}).
// Watch namespaces for enforcing opa constraints.
Watches(&source.Kind{Type: &corev1.Namespace{}}, &handler.EnqueueRequestForObject{}).
Owns(&corev1.Service{}).
Owns(&v1beta1.Ingress{}).
Owns(&netv1.NetworkPolicy{})
istioInstalled := true
for _, t := range istioTypes {
installed, err := r.resourceInstalled(context.Background(), t.Resource)
if err != nil {
return fmt.Errorf("checking if istio crd is installed: %v", err)
}
if !installed {
istioInstalled = false
break
}
}
log.Info("Determined istio installation status", "installed", istioInstalled)
if istioInstalled {
for _, t := range istioTypes {
c.Owns(gvkObject(t.Kind))
}
}
return c.Complete(r)
}
// resourceInstalled checks if a CRD is installed on the cluster.
func (r *AppEnvConfigTemplateV2Reconciler) resourceInstalled(ctx context.Context, gvr schema.GroupVersionResource) (bool, error) {
c := r.Dynamic.Resource(schema.GroupVersionResource{
Group: "apiextensions.k8s.io",
Version: "v1beta1",
Resource: "customresourcedefinitions",
})
_, err := c.Get(gvr.Resource+"."+gvr.Group, metav1.GetOptions{})
if err != nil {
if errors.IsNotFound(err) {
return false, nil
}
return false, err
}
return true, nil
}
// gvkObject returns an empty object with its GroupVersionKind set.
func gvkObject(gvk schema.GroupVersionKind) runtime.Object {
unst := &unstructured.Unstructured{}
unst.SetGroupVersionKind(gvk)
return unst
}
// getConfig currenly returns a hardcoded default configuration.
// TODO: Consider pulling from a kube ConfigMap resources instead.
func (r *AppEnvConfigTemplateV2Reconciler) getConfig() (Config, error) {
return defaultConfig, nil
}
// istioAutoInjectEnabled checks the current namespace for the
// "istio-injection" = "enabled" label.
func (r *AppEnvConfigTemplateV2Reconciler) istioAutoInjectEnabled(ctx context.Context, namespace string) (bool, error) {
name := types.NamespacedName{Name: namespace}
ns := &corev1.Namespace{}
if err := r.Client.Get(ctx, name, ns); err != nil {
return false, err
}
return ns.Labels["istio-injection"] == "enabled", nil
}
// opaNamespaces returns a list of namespaces to enforce opa constraints on.
func (r *AppEnvConfigTemplateV2Reconciler) opaNamespaces(ctx context.Context) ([]string, error) {
names := make([]string, 0)
var list corev1.NamespaceList
if err := r.Client.List(ctx, &list, client.MatchingLabels(map[string]string{
"mutating-create-update-pod-appconfig-cft-dev": "enabled",
})); err != nil {
return nil, err
}
for _, ns := range list.Items {
names = append(names, ns.Name)
}
return names, nil
}
// upsertUnstructured creates/updates unstructured objects based on spec
// comparisons.
func (r *AppEnvConfigTemplateV2Reconciler) upsertUnstructured(
ctx context.Context,
desired *unstructured.Unstructured,
gvr schema.GroupVersionResource,
namespaced bool,
) error {
var client dynamic.ResourceInterface
if namespaced {
client = r.Dynamic.Resource(gvr).Namespace(desired.GetNamespace())
} else {
client = r.Dynamic.Resource(gvr)
}
found, err := client.Get(desired.GetName(), metav1.GetOptions{})
if err != nil {
if errors.IsNotFound(err) {
log.Info("Creating", "resource", gvr.Resource, "name", desired.GetName())
if _, err := client.Create(desired, metav1.CreateOptions{}); err != nil {
return fmt.Errorf("creating: %v", err)
}
return nil
}
return fmt.Errorf("getting: %v", err)
}
if !reflect.DeepEqual(desired.Object["spec"], found.Object["spec"]) {
found.Object["spec"] = desired.Object["spec"]
log.Info("Updating", "resource", gvr.Resource, "name", desired.GetName())
if _, err := client.Update(found, metav1.UpdateOptions{}); err != nil {
return fmt.Errorf("updating: %v", err)
}
return nil
}
return nil
}
// garbageCollect removes objects that are not contained within the
// provided map.
func (r *AppEnvConfigTemplateV2Reconciler) garbageCollect(
t *appconfig.AppEnvConfigTemplateV2,
names map[types.NamespacedName]bool,
gvr schema.GroupVersionResource,
) error {
list, err := r.Dynamic.Resource(gvr).List(metav1.ListOptions{})
if err != nil {
return fmt.Errorf("listing: %v", err)
}
for _, item := range list.Items {
if !metav1.IsControlledBy(&item, t) {
continue
}
nn := types.NamespacedName{Name: item.GetName(), Namespace: item.GetNamespace()}
if !names[nn] {
log.Info("Deleting", "resource", gvr.Resource, "name", nn.Name)
if err := r.Dynamic.Resource(gvr).
Namespace(nn.Namespace).
Delete(nn.Name, &metav1.DeleteOptions{}); err != nil {
return fmt.Errorf("deleting: %v", err)
}
}
}
return nil
}