Skip to content

Commit

Permalink
feat(transfer): implement isUpToDate for transfer server
Browse files Browse the repository at this point in the history
Signed-off-by: Kevin Kendzia <[email protected]>
  • Loading branch information
kkendzia committed Feb 3, 2025
1 parent ddc4e0e commit db73779
Show file tree
Hide file tree
Showing 9 changed files with 470 additions and 1 deletion.
6 changes: 6 additions & 0 deletions apis/transfer/generator-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ ignore:
- Profile
resources:
Server:
fields:
HostKeyFingerprint:
is_read_only: true
from:
operation: DescribeServer
path: Server.HostKeyFingerprint
exceptions:
errors:
404:
Expand Down
5 changes: 5 additions & 0 deletions apis/transfer/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions apis/transfer/v1alpha1/zz_server.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ require (
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.18.0
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.21.0
golang.org/x/sync v0.7.0
gopkg.in/alecthomas/kingpin.v2 v2.2.6
k8s.io/api v0.29.1
Expand Down Expand Up @@ -124,7 +125,6 @@ require (
github.com/src-d/gcfg v1.4.0 // indirect
github.com/xanzy/ssh-agent v0.2.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.23.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions package/crds/transfer.aws.crossplane.io_servers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,12 @@ spec:
atProvider:
description: ServerObservation defines the observed state of Server
properties:
hostKeyFingerprint:
description: |-
Specifies the Base64-encoded SHA256 fingerprint of the server's host key.
This value is equivalent to the output of the ssh-keygen -l -f my-new-server-key
command.
type: string
serverID:
description: The service-assigned identifier of the server that
is created.
Expand Down
3 changes: 3 additions & 0 deletions pkg/controller/transfer/server/custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,8 @@ func (c *customConnector) Connect(ctx context.Context, mg cpresource.Managed) (m
external.preObserve = preObserve
external.preDelete = preDelete
external.preCreate = preCreate
external.isUpToDate = isUpToDate
external.lateInitialize = lateInitialize
external.postUpdate = custom.postUpdate
return external, nil
}
261 changes: 261 additions & 0 deletions pkg/controller/transfer/server/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ package server
import (
"context"
"fmt"
"slices"
"strings"

"github.com/aws/aws-sdk-go/service/ec2"
svcsdk "github.com/aws/aws-sdk-go/service/transfer"
Expand All @@ -27,11 +29,16 @@ import (
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

svcapitypes "github.com/crossplane-contrib/provider-aws/apis/transfer/v1alpha1"
"github.com/crossplane-contrib/provider-aws/apis/v1alpha1"
"github.com/crossplane-contrib/provider-aws/pkg/controller/transfer/utils"
"github.com/crossplane-contrib/provider-aws/pkg/features"
"github.com/crossplane-contrib/provider-aws/pkg/utils/pointer"
custommanaged "github.com/crossplane-contrib/provider-aws/pkg/utils/reconciler/managed"
Expand Down Expand Up @@ -133,6 +140,45 @@ func (c *custom) postObserve(_ context.Context, cr *svcapitypes.Server, obj *svc
return obs, nil
}

func (h *custom) postUpdate(ctx context.Context, cr *svcapitypes.Server, resp *svcsdk.UpdateServerOutput, upd managed.ExternalUpdate, err error) (managed.ExternalUpdate, error) {
if err != nil {
return managed.ExternalUpdate{}, err
}

// Tag update needs to separate because UpdateAddon does not include tags (for unknown reason).

desc, err := h.client.DescribeServerWithContext(ctx, &svcsdk.DescribeServerInput{
ServerId: resp.ServerId,
})
if err != nil || desc.Server == nil {
return managed.ExternalUpdate{}, errors.Wrap(err, errDescribe)
}

isUpToDate, add, remove := utils.DiffTags(cr.Spec.ForProvider.Tags, desc.Server.Tags)
if isUpToDate {
return managed.ExternalUpdate{}, nil
}
if len(add) > 0 {
_, err := h.client.TagResourceWithContext(ctx, &svcsdk.TagResourceInput{
Arn: desc.Server.Arn,
Tags: add,
})
if err != nil {
return managed.ExternalUpdate{}, errors.Wrap(err, "cannot tag resource")
}
}
if len(remove) > 0 {
_, err := h.client.UntagResourceWithContext(ctx, &svcsdk.UntagResourceInput{
Arn: desc.Server.Arn,
TagKeys: remove,
})
if err != nil {
return managed.ExternalUpdate{}, errors.Wrap(err, "cannot tag resource")
}
}
return managed.ExternalUpdate{}, nil
}

func postCreate(_ context.Context, cr *svcapitypes.Server, obj *svcsdk.CreateServerOutput, cre managed.ExternalCreation, err error) (managed.ExternalCreation, error) {
if err != nil {
return managed.ExternalCreation{}, err
Expand Down Expand Up @@ -186,3 +232,218 @@ func (c *custom) DescribeVpcEndpoint(obj *svcsdk.DescribeServerOutput) (dnsEntri
}
return []*ec2.VpcEndpoint{}, nil
}

func lateInitialize(spec *svcapitypes.ServerParameters, obj *svcsdk.DescribeServerOutput) error {
if spec.ProtocolDetails == nil {
spec.ProtocolDetails = &svcapitypes.ProtocolDetails{}
}
if obj.Server.ProtocolDetails != nil {
if spec.ProtocolDetails.As2Transports == nil {
spec.ProtocolDetails.As2Transports = obj.Server.ProtocolDetails.As2Transports
}
if spec.ProtocolDetails.PassiveIP == nil {
spec.ProtocolDetails.PassiveIP = obj.Server.ProtocolDetails.PassiveIp
}
if spec.ProtocolDetails.SetStatOption == nil {
spec.ProtocolDetails.SetStatOption = obj.Server.ProtocolDetails.SetStatOption
}
if spec.ProtocolDetails.TLSSessionResumptionMode == nil {
spec.ProtocolDetails.TLSSessionResumptionMode = obj.Server.ProtocolDetails.TlsSessionResumptionMode
}
}
if spec.CustomEndpointDetails != nil && spec.CustomEndpointDetails.VPCEndpointID == nil && obj.Server.EndpointDetails.VpcEndpointId != nil {
spec.CustomEndpointDetails.VPCEndpointID = obj.Server.EndpointDetails.VpcEndpointId
}
return nil
}

func isUpToDate(_ context.Context, cr *svcapitypes.Server, cur *svcsdk.DescribeServerOutput) (bool, string, error) {
in := cr.Spec.ForProvider
out := cur.Server

if isNotUpToDate(in, out) {
return false, "", nil
}

return true, "", nil

}

func isNotUpToDate(in svcapitypes.ServerParameters, out *svcsdk.DescribedServer) bool { //nolint:gocyclo
if !cmp.Equal(in.Certificate, out.Certificate) {
return true
}

if !cmp.Equal(in.EndpointType, out.EndpointType) {
return true
}

if !cmp.Equal(in.IdentityProviderType, out.IdentityProviderType) {
return true
}

if !cmp.Equal(in.LoggingRole, out.LoggingRole) {
return true
}

if !cmp.Equal(in.PostAuthenticationLoginBanner, out.PostAuthenticationLoginBanner) {
return true
}

if !cmp.Equal(in.PreAuthenticationLoginBanner, out.PreAuthenticationLoginBanner) {
return true
}

if !cmp.Equal(in.SecurityPolicyName, out.SecurityPolicyName) {
return true
}

if !cmp.Equal(in.StructuredLogDestinations, out.StructuredLogDestinations) {
return true
}

if !isIdentityProviderDetailsUpToDate(in.IdentityProviderDetails, out.IdentityProviderDetails) {
return true
}

if !isProtocolDetailsUpToDate(in.ProtocolDetails, out.ProtocolDetails) {
return true
}

if !isCustomEndpointUpToDate(in.CustomEndpointDetails, out.EndpointDetails) {
return true
}

if !isWorkflowDetailsUpToDate(in.WorkflowDetails, out.WorkflowDetails) {
return true
}

if upToDate, _, _ := utils.DiffTags(in.Tags, out.Tags); !upToDate {
return true
}

if !isHostKeyUpToDate(in.HostKey, out.HostKeyFingerprint) {
return true
}

return false
}

func isIdentityProviderDetailsUpToDate(in *svcapitypes.IdentityProviderDetails, out *svcsdk.IdentityProviderDetails) bool {
if in == nil && out == nil {
return true
}
if in == nil || out == nil {
return false
}
if !cmp.Equal(in.DirectoryID, out.DirectoryId) {
return false
}
if !cmp.Equal(in.Function, out.Function) {
return false
}
if !cmp.Equal(in.InvocationRole, out.InvocationRole) {
return false
}
if !cmp.Equal(in.SftpAuthenticationMethods, out.SftpAuthenticationMethods) {
return false
}
if !cmp.Equal(in.URL, out.Url) {
return false
}
return true
}

func isProtocolDetailsUpToDate(in *svcapitypes.ProtocolDetails, out *svcsdk.ProtocolDetails) bool {
if in == nil && out == nil {
return true
}
if in == nil || out == nil {
return false
}
if !cmp.Equal(in.As2Transports, out.As2Transports) {
return false
}
if !cmp.Equal(in.PassiveIP, out.PassiveIp) {
return false
}
if !cmp.Equal(in.SetStatOption, out.SetStatOption) {
return false
}
if !cmp.Equal(in.TLSSessionResumptionMode, out.TlsSessionResumptionMode) {
return false
}
return true
}

func isCustomEndpointUpToDate(in *svcapitypes.CustomEndpointDetails, out *svcsdk.EndpointDetails) bool {
if in == nil && out == nil {
return true
}
if in == nil || out == nil {
return false
}
if !cmp.Equal(in.AddressAllocationIDs, out.AddressAllocationIds) {
return false
}
if !cmp.Equal(in.SubnetIDs, out.SubnetIds) {
return false
}
if !cmp.Equal(in.VPCEndpointID, out.VpcEndpointId) {
return false
}
if !cmp.Equal(in.VPCID, out.VpcId) {
return false
}
return true
}

func isWorkflowDetailsUpToDate(in *svcapitypes.WorkflowDetails, out *svcsdk.WorkflowDetails) bool { //nolint:gocyclo
if in == nil && out == nil {
return true
}
if in == nil || out == nil {
return false
}
if len(in.OnPartialUpload) != len(out.OnPartialUpload) || len(in.OnUpload) != len(out.OnUpload) {
return false
}

if len(in.OnPartialUpload) == 0 && len(in.OnUpload) == 0 {
return true
}

apiTypesSort := func(a *svcapitypes.WorkflowDetail, b *svcapitypes.WorkflowDetail) int {
return strings.Compare(*a.WorkflowID, *b.WorkflowID)
}
sdkSort := func(a *svcsdk.WorkflowDetail, b *svcsdk.WorkflowDetail) int {
return strings.Compare(*a.WorkflowId, *b.WorkflowId)
}
compareApiSdk := func(a *svcapitypes.WorkflowDetail, b *svcsdk.WorkflowDetail) bool {
return ptr.Deref(a.ExecutionRole, "") == ptr.Deref(b.ExecutionRole, "") && ptr.Deref(a.WorkflowID, "") == ptr.Deref(b.WorkflowId, "")
}

slices.SortFunc(in.OnPartialUpload, apiTypesSort)
slices.SortFunc(in.OnUpload, apiTypesSort)
slices.SortFunc(out.OnPartialUpload, sdkSort)
slices.SortFunc(out.OnUpload, sdkSort)

return slices.EqualFunc(in.OnPartialUpload, out.OnPartialUpload, compareApiSdk) && slices.EqualFunc(in.OnUpload, out.OnUpload, compareApiSdk)
}

func isHostKeyUpToDate(in *string, out *string) bool {
// if there is no HostKey set, AWS generates a HostKey by itself. if the HostKey gets deleted from the spec, don't update.
if in == nil {
return true
}
if out == nil {
return false
}
key, err := ssh.ParsePrivateKey([]byte(ptr.Deref(in, "")))
if err != nil {
panic(err)
}
fingerprint := ssh.FingerprintSHA256(key.PublicKey())
currentFingerprint := strings.TrimSuffix(ptr.Deref(out, ""), "=")
return fingerprint == currentFingerprint
}
Loading

0 comments on commit db73779

Please sign in to comment.