package mesh import ( "context" appmesh "github.com/aws/aws-app-mesh-controller-for-k8s/apis/appmesh/v1beta2" "github.com/aws/aws-app-mesh-controller-for-k8s/pkg/aws/services" "github.com/aws/aws-app-mesh-controller-for-k8s/pkg/conversions" "github.com/aws/aws-app-mesh-controller-for-k8s/pkg/k8s" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" appmeshsdk "github.com/aws/aws-sdk-go/service/appmesh" "github.com/go-logr/logr" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/conversion" "sigs.k8s.io/controller-runtime/pkg/client" ) // ResourceManager is dedicated to manage AppMesh Mesh resources for k8s Mesh CRs. type ResourceManager interface { // Reconcile will create/update AppMesh Mesh to match ms.spec, and update ms.status Reconcile(ctx context.Context, ms *appmesh.Mesh) error // Cleanup will delete AppMesh Mesh created for ms. Cleanup(ctx context.Context, ms *appmesh.Mesh) error } func NewDefaultResourceManager( k8sClient client.Client, appMeshSDK services.AppMesh, accountID string, log logr.Logger) ResourceManager { return &defaultResourceManager{ k8sClient: k8sClient, appMeshSDK: appMeshSDK, accountID: accountID, log: log, } } // defaultResourceManager implements ResourceManager type defaultResourceManager struct { k8sClient client.Client appMeshSDK services.AppMesh // current iam identity's aws accountID, used to differentiate mesh ownership. accountID string log logr.Logger } func (m *defaultResourceManager) Reconcile(ctx context.Context, ms *appmesh.Mesh) error { sdkMS, err := m.findSDKMesh(ctx, ms) if err != nil { return err } if sdkMS == nil { sdkMS, err = m.createSDKMesh(ctx, ms) if err != nil { return err } } else { sdkMS, err = m.updateSDKMesh(ctx, sdkMS, ms) if err != nil { return err } } return m.updateCRDMesh(ctx, ms, sdkMS) } func (m *defaultResourceManager) Cleanup(ctx context.Context, ms *appmesh.Mesh) error { sdkMS, err := m.findSDKMesh(ctx, ms) if err != nil { if ms.Status.MeshARN == nil { return nil } return err } if sdkMS == nil { return nil } return m.deleteSDKMesh(ctx, sdkMS, ms) } func (m *defaultResourceManager) findSDKMesh(ctx context.Context, ms *appmesh.Mesh) (*appmeshsdk.MeshData, error) { resp, err := m.appMeshSDK.DescribeMeshWithContext(ctx, &appmeshsdk.DescribeMeshInput{ MeshName: ms.Spec.AWSName, MeshOwner: ms.Spec.MeshOwner, }) if err != nil { if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NotFoundException" { return nil, nil } return nil, err } return resp.Mesh, nil } func (m *defaultResourceManager) createSDKMesh(ctx context.Context, ms *appmesh.Mesh) (*appmeshsdk.MeshData, error) { sdkMSSpec, err := BuildSDKMeshSpec(ctx, ms) if err != nil { return nil, err } resp, err := m.appMeshSDK.CreateMeshWithContext(ctx, &appmeshsdk.CreateMeshInput{ MeshName: ms.Spec.AWSName, Spec: sdkMSSpec, }) if err != nil { return nil, err } return resp.Mesh, nil } func (m *defaultResourceManager) updateSDKMesh(ctx context.Context, sdkMS *appmeshsdk.MeshData, ms *appmesh.Mesh) (*appmeshsdk.MeshData, error) { actualSDKMSSpec := sdkMS.Spec desiredSDKMSSpec, err := BuildSDKMeshSpec(ctx, ms) if err != nil { return nil, err } opts := cmpopts.EquateEmpty() if cmp.Equal(desiredSDKMSSpec, actualSDKMSSpec, opts) { return sdkMS, nil } if !m.isSDKMeshControlledByCRDMesh(ctx, sdkMS, ms) { m.log.V(1).Info("skip mesh update since it's not controlled", "mesh", k8s.NamespacedName(ms), "meshARN", aws.StringValue(sdkMS.Metadata.Arn), ) return sdkMS, nil } diff := cmp.Diff(desiredSDKMSSpec, actualSDKMSSpec, opts) m.log.V(1).Info("meshSpec changed", "mesh", k8s.NamespacedName(ms), "actualSDKMSSpec", actualSDKMSSpec, "desiredSDKMSSpec", desiredSDKMSSpec, "diff", diff, ) resp, err := m.appMeshSDK.UpdateMeshWithContext(ctx, &appmeshsdk.UpdateMeshInput{ MeshName: sdkMS.MeshName, Spec: desiredSDKMSSpec, }) if err != nil { return nil, err } return resp.Mesh, nil } func (m *defaultResourceManager) deleteSDKMesh(ctx context.Context, sdkMS *appmeshsdk.MeshData, ms *appmesh.Mesh) error { if !m.isSDKMeshOwnedByCRDMesh(ctx, sdkMS, ms) { m.log.V(1).Info("skip mesh deletion since its not owned", "mesh", k8s.NamespacedName(ms), "meshARN", aws.StringValue(sdkMS.Metadata.Arn), ) return nil } _, err := m.appMeshSDK.DeleteMeshWithContext(ctx, &appmeshsdk.DeleteMeshInput{ MeshName: sdkMS.MeshName, }) if err != nil { return err } return nil } func (m *defaultResourceManager) updateCRDMesh(ctx context.Context, ms *appmesh.Mesh, sdkMS *appmeshsdk.MeshData) error { oldMS := ms.DeepCopy() needsUpdate := false if aws.StringValue(ms.Status.MeshARN) != aws.StringValue(sdkMS.Metadata.Arn) { ms.Status.MeshARN = sdkMS.Metadata.Arn needsUpdate = true } if aws.Int64Value(ms.Status.ObservedGeneration) != ms.Generation { ms.Status.ObservedGeneration = aws.Int64(ms.Generation) needsUpdate = true } msActiveConditionStatus := corev1.ConditionFalse if sdkMS.Status != nil && aws.StringValue(sdkMS.Status.Status) == appmeshsdk.MeshStatusCodeActive { msActiveConditionStatus = corev1.ConditionTrue } if updateCondition(ms, appmesh.MeshActive, msActiveConditionStatus, nil, nil) { needsUpdate = true } if !needsUpdate { return nil } return m.k8sClient.Status().Patch(ctx, ms, client.MergeFrom(oldMS)) } // isSDKMeshControlledByCRDMesh checks whether an AppMesh mesh is controlled by CRDMesh // if it's controlled, CRDMesh update is responsible for update AppMesh mesh. func (m *defaultResourceManager) isSDKMeshControlledByCRDMesh(ctx context.Context, sdkMS *appmeshsdk.MeshData, ms *appmesh.Mesh) bool { if aws.StringValue(sdkMS.Metadata.ResourceOwner) != m.accountID { return false } return true } // isSDKMeshOwnedByCRDMesh checks whether an AppMesh mesh is owned by CRDMesh. // if it's owned, CRDMesh deletion is responsible for delete AppMesh mesh. func (m *defaultResourceManager) isSDKMeshOwnedByCRDMesh(ctx context.Context, sdkMS *appmeshsdk.MeshData, ms *appmesh.Mesh) bool { if !m.isSDKMeshControlledByCRDMesh(ctx, sdkMS, ms) { return false } // TODO: Adding tagging support, so a existing mesh in owner account but not ownership can be support. // currently, mesh controllership == ownership, but it don't have to be so once we add tagging support. return true } func BuildSDKMeshSpec(ctx context.Context, ms *appmesh.Mesh) (*appmeshsdk.MeshSpec, error) { converter := conversion.NewConverter(conversion.DefaultNameFunc) converter.RegisterUntypedConversionFunc((*appmesh.MeshSpec)(nil), (*appmeshsdk.MeshSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return conversions.Convert_CRD_MeshSpec_To_SDK_MeshSpec(a.(*appmesh.MeshSpec), b.(*appmeshsdk.MeshSpec), scope) }) sdkMSSpec := &appmeshsdk.MeshSpec{} if err := converter.Convert(&ms.Spec, sdkMSSpec, nil); err != nil { return nil, err } return sdkMSSpec, nil }