Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Base migration framework #119

Merged
merged 3 commits into from
Dec 14, 2022
Merged

Conversation

ulucinar
Copy link
Collaborator

@ulucinar ulucinar commented Oct 24, 2022

Description of your changes

Fixes https://github.com/upbound/team-extensions/issues/29

This PR introduces the base framework for migrating from community providers to official providers.

I have:

  • Read and followed Crossplane's contribution process.
  • Run make reviewable to ensure this PR is ready for review.
  • Add tests for conversion functions, plan generator, etc.
  • Added backport release-x.y labels to auto-backport this PR if necessary.

How has this code been tested

Manually tested using the implemented sample converter and the following manifests:

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xmyresources.test.com
spec:
  claimNames:
    kind: MyResource
    plural: myresources
  group: test.com
  names:
    kind: XMyResource
    plural: xmyresources
  versions:
  - name: v1alpha1
    referenceable: true
    schema:
      openAPIV3Schema:
        properties:
          spec:
            properties:
              parameters:
                properties:
                  tagValue:
                    type: string
                required:
                - tagValue
                type: object
            required:
            - parameters
            type: object
        type: object
    served: true

---

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  labels:
    purpose: example
  name: example
spec:
  compositeTypeRef:
    apiVersion: test.com/v1alpha1
    kind: XMyResource
  resources:
    - base:
        apiVersion: ec2.aws.crossplane.io/v1beta1
        kind: VPC
        spec:
          forProvider:
            cidrBlock: "192.168.0.0/16"
            ipv6Pool: pool1
            region: "West Europe"
            tags:
              - key: key1
                value: val1
              - key: key2
                value: val2
              - key: key3
                value: val3
      name: vpc
      patches:
        - fromFieldPath: "spec.parameters.tagValue"
          toFieldPath: spec.forProvider.tags[0].value
        - fromFieldPath: "spec.parameters.tagValue"
          toFieldPath: spec.forProvider.tags[1].value
        - fromFieldPath: "spec.parameters.tagValue"
          toFieldPath: spec.forProvider.tags[2].value

---

apiVersion: test.com/v1alpha1
kind: MyResource
metadata:
  name: my-resource
  namespace: upbound-system
spec:
  compositionRef:
    name: example
  parameters:
    tagValue: demo-test

---

apiVersion: ec2.aws.crossplane.io/v1beta1
kind: VPC
metadata:
  name: test-vpc
spec:
  forProvider:
    cidrBlock: 192.168.0.0/16
    ipv6Pool: pool1
    region: West Europe
    tags:
      - key: key1
        value: val1
      - key: key2
        value: val2
      - key: key3
        value: val3
status:
  atProvider:
    vpcId: vpc-0391605a3bf7977dd

And the following sample converter for community provider VPC resources (under pkg/migration/converters/vpc_converter.go):

func resources(mg resource.Managed) ([]resource.Managed, error) {
	source := mg.(*srcv1beta1.VPC)
	target := &targetv1beta1.VPC{}
	if _, err := migration.CopyInto(source, target, targetv1beta1.VPC_GroupVersionKind, "spec.forProvider.tags"); err != nil {
		return nil, errors.Wrap(err, "failed to copy source into target")
	}
	target.Spec.ForProvider.Tags = make(map[string]*string, len(source.Spec.ForProvider.Tags))
	for _, t := range source.Spec.ForProvider.Tags {
		v := t.Value
		target.Spec.ForProvider.Tags[t.Key] = &v
	}
	return []resource.Managed{
		target,
	}, nil
}

func composedTemplates(cmp v1.ComposedTemplate, convertedBase ...*v1.ComposedTemplate) error {
	for i, cb := range convertedBase {
		for j, p := range cb.Patches {
			if p.ToFieldPath == nil || !strings.HasPrefix(*p.ToFieldPath, "spec.forProvider.tags") {
				continue
			}
			u, err := migration.FromRawExtension(cmp.Base)
			if err != nil {
				return errors.Wrap(err, "failed to convert ComposedTemplate with VPC base")
			}
			paved := fieldpath.Pave(u.Object)
			key, err := paved.GetString(strings.ReplaceAll(*p.ToFieldPath, ".value", ".key"))
			if err != nil {
				return errors.Wrap(err, "failed to get value from paved")
			}
			s := fmt.Sprintf(`spec.forProvider.tags["%s"]`, key)
			convertedBase[i].Patches[j].ToFieldPath = &s
		}
	}
	return nil
}

The converted resources are as follows:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  labels:
    purpose: example
  name: example
spec:
  compositeTypeRef:
    apiVersion: test.com/v1alpha1
    kind: XMyResource
  resources:
  - base:
      apiVersion: ec2.aws.upbound.io/v1beta1
      kind: VPC
      spec:
        forProvider:
          cidrBlock: 192.168.0.0/16
          region: West Europe
          tags:
            key1: val1
            key2: val2
            key3: val3
    name: vpc
    patches:
    - fromFieldPath: spec.parameters.tagValue
      toFieldPath: spec.forProvider.tags["key1"]
    - fromFieldPath: spec.parameters.tagValue
      toFieldPath: spec.forProvider.tags["key2"]
    - fromFieldPath: spec.parameters.tagValue
      toFieldPath: spec.forProvider.tags["key3"]

---

apiVersion: ec2.aws.upbound.io/v1beta1
kind: VPC
metadata:
  name: test-vpc
spec:
  forProvider:
    cidrBlock: 192.168.0.0/16
    region: West Europe
    tags:
      key1: val1
      key2: val2
      key3: val3

Copy link
Member

@sergenyalcin sergenyalcin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ulucinar I left a few comments.

pkg/migration/plan_generator.go Outdated Show resolved Hide resolved
pkg/migration/converter.go Show resolved Hide resolved
pkg/migration/converter.go Show resolved Hide resolved
@ulucinar ulucinar force-pushed the wearemoving branch 2 times, most recently from caae7a6 to acbea16 Compare October 28, 2022 15:52
@ulucinar ulucinar force-pushed the wearemoving branch 12 times, most recently from 8508801 to c0f5204 Compare November 2, 2022 20:40
Copy link
Member

@muvaf muvaf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ulucinar ! Loved how much documentation the code has! I have a couple of non-nit comments but at a high level, it looks pretty good!

ComposedTemplates(cmp v1.ComposedTemplate, convertedBase ...*v1.ComposedTemplate) error
}

// Source is a source for reading resource manifests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need the abstractions over source and target? AFAIK, there are two main scenarios we'd like to handle;

  • Running against live Kubernetes cluster.
    • Read resources from cluster and write to the filesystem, prepare a migration plan on filesystem, execute it against the cluster.
  • Running against manifests stored in a Git repo, i.e. GitOps, hence filesystem.
    • The files are already on the filesystem, prepare a migration plan on filesystem, skip the execution since the files will be committed.

So, it feels like if we have a seperate utility to get resources from cluster to filesystem that can be turned off by a flag, then the rest of the steps are already normalized to work on the filesystem if I'm not missing something.

// Example: resources/a.yaml:resources/b.yaml
Parents string
// IsComposite set if the object belongs to a Composite type
IsComposite bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Category string, which is used in CRDs as well, as single field could help here to classify.

// together with the associated Metadata.
type UnstructuredWithMetadata struct {
Object unstructured.Unstructured
Metadata Metadata
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Metadata word is overloaded in this context. Can we have another name? Descriptor maybe?

pg.Plan.Spec.Steps[stepDeleteOldManaged].Name = "delete-old-managed"
pg.Plan.Spec.Steps[stepDeleteOldManaged].Type = StepTypeDelete
deletePolicy := FinalizerPolicyRemove
pg.Plan.Spec.Steps[stepDeleteOldManaged].Delete = &DeleteStep{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should let the controller do a graceful orphaned deletion so that we stay on the normal path of execution. For example, unpublishing connection details is something done in orphan deletion where we'd not get to have.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will check this in a future iteration after testing the behavior. Thank you!

annot = make(map[string]string)
}
annot[meta.AnnotationKeyReconciliationPaused] = "true"
u.SetAnnotations(annot)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: There is meta.AddAnnotations function in runtime that would help here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +423 to +426
if err := pg.target.Put(*u); err != nil {
return errors.Wrap(err, errResourceOutput)
}
return nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if err := pg.target.Put(*u); err != nil {
return errors.Wrap(err, errResourceOutput)
}
return nil
return errors.Wrap(pg.target.Put(*u), errResourceOutput)

nit

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
}
default:
if o.Metadata.IsComposite {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: handling these IsComposite and IsClaim as cases in switch could make it easier to read.

// unstructured.Unstructured. Before the converted object is
// returned, it's sanitized by removing certain fields
// (like status, metadata.creationTimestamp).
func ToUnstructured(mg any) unstructured.Unstructured {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we name this something like ToSanitizedUnstructured or ToSanitized? It could be misleading otherwise since there is a known ToUnstructured method in upstream libraries.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


var (
// the default Converter registry
registry Registry = make(map[schema.GroupVersionKind]Converter)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'd lean towards leaving it to the consumer of this package to initialize and maintain their Registry object instead of us providing a global map unless we have to.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Planning to address in the next iteration.

Path string
// colon separated list of parent `Path`s for fan-ins and fan-outs
// Example: resources/a.yaml:resources/b.yaml
Parents string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this field isn't used anywhere. Is it a leftover?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field is to be employed by the source and target implementations.

@Upbound-CLA
Copy link

Upbound-CLA commented Nov 23, 2022

CLA assistant check
All committers have signed the CLA.

Copy link
Member

@muvaf muvaf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of my comments were non-blocking, feel free to go ahead! Thanks @ulucinar !

Signed-off-by: Alper Rifat Ulucinar <ulucinar@users.noreply.github.com>
Signed-off-by: Alper Rifat Ulucinar <ulucinar@users.noreply.github.com>
@ulucinar ulucinar force-pushed the wearemoving branch 3 times, most recently from 2b26008 to 81b18ac Compare December 13, 2022 22:06
@ulucinar ulucinar force-pushed the wearemoving branch 3 times, most recently from d1b3cc2 to af81b4a Compare December 13, 2022 22:21
Signed-off-by: Alper Rifat Ulucinar <ulucinar@users.noreply.github.com>
@jeanduplessis jeanduplessis merged commit 52337e6 into crossplane:main Dec 14, 2022
@ulucinar ulucinar deleted the wearemoving branch December 14, 2022 07:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants