Skip to content

Add configuration.meta/crossplane.yaml support for dependencies to beta validate#5815

Merged
jbw976 merged 5 commits intocrossplane:masterfrom
enesonus:config-meta-validate
Jul 22, 2024
Merged

Add configuration.meta/crossplane.yaml support for dependencies to beta validate#5815
jbw976 merged 5 commits intocrossplane:masterfrom
enesonus:config-meta-validate

Conversation

@enesonus
Copy link
Contributor

@enesonus enesonus commented Jul 4, 2024

Description of your changes

This PR enables crossplane beta validate command to use Configuration.meta/crossplane.yaml for pulling dependencies. With this PR beta validate command can validate the correctness of the Configurations using their crossplane.yaml file before the build, without an already published Configuration.pkg.

Here is what this PR does:

Current usage (with https://github.com/enesonus/configuration-aws-eks/tree/config-meta-validate):

We need an already built and published Configuration.pkg specified at examples/configuration.yaml

crossplane beta render examples/eks-xr.yaml apis/composition.yaml examples/functions.yaml --include-full-xr | crossplane beta validate examples/configuration.yaml -
 
package schemas does not exist, downloading:  xpkg.upbound.io/crossplane-contrib/provider-kubernetes:v0.12.1
package schemas does not exist, downloading:  xpkg.upbound.io/upbound/provider-aws-ec2:v1.2.0
package schemas does not exist, downloading:  xpkg.upbound.io/upbound/provider-aws-eks:v1.2.0
package schemas does not exist, downloading:  xpkg.upbound.io/upbound/provider-aws-iam:v1.2.0
package schemas does not exist, downloading:  xpkg.upbound.io/crossplane-contrib/function-patch-and-transform:v0.4.0
package schemas does not exist, downloading:  xpkg.upbound.io/upbound/configuration-aws-eks:v0.11.0
package schemas does not exist, downloading:  xpkg.upbound.io/upbound/configuration-aws-network:v0.12.0
package schemas does not exist, downloading:  xpkg.upbound.io/crossplane-contrib/provider-helm:v0.17.0
[x] schema validation error aws.platform.upbound.io/v1alpha1, Kind=XEKS, configuration-aws-eks : spec.parameters.deletionPolicy: Required value
[x] schema validation error aws.platform.upbound.io/v1alpha1, Kind=XEKS, configuration-aws-eks : spec.parameters.providerConfigName: Required value
[✓] kubernetes.crossplane.io/v1alpha2, Kind=Object, configuration-aws-eks-aws-auth validated successfully
[✓] iam.aws.upbound.io/v1beta1, Kind=RolePolicyAttachment, clusterRolePolicyAttachment validated successfully
[✓] iam.aws.upbound.io/v1beta1, Kind=RolePolicyAttachment, cniRolePolicyAttachment validated successfully
[✓] iam.aws.upbound.io/v1beta1, Kind=RolePolicyAttachment, containerRegistryRolePolicyAttachment validated successfully
[✓] iam.aws.upbound.io/v1beta1, Kind=Role, controlplaneRole validated successfully
[✓] iam.aws.upbound.io/v1beta1, Kind=RolePolicyAttachment, ebsCsiRolePolicyAttachment validated successfully
[✓] kubernetes.crossplane.io/v1alpha2, Kind=Object, configuration-aws-eks-irsa-settings validated successfully
[✓] eks.aws.upbound.io/v1beta1, Kind=Cluster, kubernetesCluster validated successfully
[x] schema validation error eks.aws.upbound.io/v1beta1, Kind=ClusterAuth, kubernetesClusterAuth : spec.writeConnectionSecretToRef.name: Required value
[✓] iam.aws.upbound.io/v1beta1, Kind=Role, nodegroupRole validated successfully
[x] schema validation error helm.crossplane.io/v1beta1, Kind=ProviderConfig, configuration-aws-eks : spec.credentials.secretRef.name: Required value
[x] schema validation error kubernetes.crossplane.io/v1alpha1, Kind=ProviderConfig, configuration-aws-eks : spec.credentials.secretRef.name: Required value
[✓] iam.aws.upbound.io/v1beta1, Kind=RolePolicyAttachment, workerNodeRolePolicyAttachment validated successfully
Total 14 resources: 0 missing schemas, 10 success cases, 4 failure cases
crossplane: error: cannot validate resources: could not validate all resources

New usage (with https://github.com/enesonus/configuration-aws-eks/tree/config-meta-validate):

No need for building and publishing the package. Pulls dependencies from Configuration.meta/crossplane.yaml

crossplane beta render examples/eks-xr.yaml apis/composition.yaml examples/functions.yaml --include-full-xr | go run cmd/crank/main.go beta validate crossplane.yaml -

package schemas does not exist, downloading:  xpkg.upbound.io/crossplane-contrib/provider-kubernetes:v0.12.1
package schemas does not exist, downloading:  xpkg.upbound.io/upbound/provider-aws-ec2:v1.2.0
package schemas does not exist, downloading:  xpkg.upbound.io/crossplane-contrib/function-patch-and-transform:v0.4.0
package schemas does not exist, downloading:  xpkg.upbound.io/crossplane-contrib/function-kcl:v0.6.0
package schemas does not exist, downloading:  xpkg.upbound.io/crossplane-contrib/function-auto-ready:v0.2.1
package schemas does not exist, downloading:  xpkg.upbound.io/crossplane-contrib/function-sequencer:v0.1.2
package schemas does not exist, downloading:  xpkg.upbound.io/upbound/configuration-aws-network:v0.12.0
package schemas does not exist, downloading:  xpkg.upbound.io/crossplane-contrib/provider-helm:v0.17.0
package schemas does not exist, downloading:  xpkg.upbound.io/upbound/provider-aws-eks:v1.2.0
package schemas does not exist, downloading:  xpkg.upbound.io/upbound/provider-aws-iam:v1.2.0
[!] could not find CRD/XRD for: aws.platform.upbound.io/v1alpha1, Kind=XEKS
[✓] kubernetes.crossplane.io/v1alpha2, Kind=Object, configuration-aws-eks-aws-auth validated successfully
[✓] iam.aws.upbound.io/v1beta1, Kind=RolePolicyAttachment, clusterRolePolicyAttachment validated successfully
[✓] iam.aws.upbound.io/v1beta1, Kind=RolePolicyAttachment, cniRolePolicyAttachment validated successfully
[✓] iam.aws.upbound.io/v1beta1, Kind=RolePolicyAttachment, containerRegistryRolePolicyAttachment validated successfully
[✓] iam.aws.upbound.io/v1beta1, Kind=Role, controlplaneRole validated successfully
[✓] iam.aws.upbound.io/v1beta1, Kind=RolePolicyAttachment, ebsCsiRolePolicyAttachment validated successfully
[✓] kubernetes.crossplane.io/v1alpha2, Kind=Object, configuration-aws-eks-irsa-settings validated successfully
[✓] eks.aws.upbound.io/v1beta1, Kind=Cluster, kubernetesCluster validated successfully
[x] schema validation error eks.aws.upbound.io/v1beta1, Kind=ClusterAuth, kubernetesClusterAuth : spec.writeConnectionSecretToRef.name: Required value
[✓] iam.aws.upbound.io/v1beta1, Kind=Role, nodegroupRole validated successfully
[x] schema validation error helm.crossplane.io/v1beta1, Kind=ProviderConfig, configuration-aws-eks : spec.credentials.secretRef.name: Required value
[x] schema validation error kubernetes.crossplane.io/v1alpha1, Kind=ProviderConfig, configuration-aws-eks : spec.credentials.secretRef.name: Required value
[✓] iam.aws.upbound.io/v1beta1, Kind=RolePolicyAttachment, workerNodeRolePolicyAttachment validated successfully
Total 14 resources: 1 missing schemas, 10 success cases, 3 failure cases
crossplane: error: cannot validate resources: could not validate all resources

Fixes #5728

I have:

  • Read and followed Crossplane's contribution process.
  • Run earthly +reviewable to ensure this PR is ready for review.
  • Added or updated unit tests.
  • Added or updated e2e tests.
  • Linked a PR or a docs tracking issue to document this change.
  • Added backport release-x.y labels to auto-backport this PR.

Need help with this checklist? See the cheat sheet.

Signed-off-by: Mehmet Enes <menes.onus@gmail.com>
@enesonus enesonus requested review from a team and phisco as code owners July 4, 2024 07:43
@enesonus enesonus requested a review from negz July 4, 2024 07:43
@enesonus enesonus changed the title Add configuration.meta support for deps to beta validate Add configuration.meta/crossplane.yaml support for dependencies to beta validate Jul 4, 2024
Copy link
Member

@ezgidemirel ezgidemirel left a comment

Choose a reason for hiding this comment

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

Thanks for the PR @enesonus! It looks good in general. I added a couple of nit comments.

)

var (
configDep2Yaml = []byte(`apiVersion: meta.pkg.crossplane.io/v1alpha1
Copy link
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
configDep2Yaml = []byte(`apiVersion: meta.pkg.crossplane.io/v1alpha1
configMetaYaml = []byte(`apiVersion: meta.pkg.crossplane.io/v1alpha1

can you also add a yaml for a configuration package?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can add a yaml for a configuration package for sure but what would be the difference between that and the second object inside the extensions arg which has pkg.crossplane.io/v1alpha1 as apiVersion?

Copy link
Member

Choose a reason for hiding this comment

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

Ohh I assumed, you're using this meta object as an input, not as an output. Crossplane packages cannot depend on meta objects. Therefore, it's safe to replace this manifest with a configuration package to verify validate command can download the dependencies which are configurations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ezgidemirel can you please check the changes made at 75380c0 to manager_test.go? I tried to follow your comments but not %100 sure if they are OK.

Copy link
Member

@jbw976 jbw976 left a comment

Choose a reason for hiding this comment

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

Something interesting I noticed is that with this approach we lose the ability to validate the XR (and other types that may be defined in the local package itself). For example:

[!] could not find CRD/XRD for: aws.platform.upbound.io/v1alpha1, Kind=XEKS

Looks like we only will get the schemas for dependencies mentioned in the crossplane.yaml file and nothing that is directly in the local package being tested. What do we think of that limitation? 🤔

Copy link
Member

@jbw976 jbw976 left a comment

Choose a reason for hiding this comment

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

Thanks @enesonus! I have some questions about how we could maybe model this data better and about how we could make the unit test easier to understand and possibly testing more in isolation so we have confidence all the cases are working correctly. 🙇‍♂️

deps map[string]bool // One level dependency images
confs map[string]bool // Configuration images
deps map[string]bool // Dependency images
confs map[string]interface{} // Configuration images
Copy link
Member

@jbw976 jbw976 Jul 10, 2024

Choose a reason for hiding this comment

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

a descriptive comment of what is expected / allowed in this map would be helpful for readers - it's a bit opaque as it is now. Just seeing this declaration, I can guess that anything is technically allowed to be inserted, but what types are going to be put into the map in a practical sense? are there any implications to call out here depending on what type gets inserted at runtime?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

75380c0 changes the type of confs to map[string]*metav1.Configuration. I think the type declaration will be descriptive enough and increase the readability of this code which is a concern of this comment as I understood.

return errors.Wrapf(err, "cannot download package %s", image)
}
switch c := m.confs[image].(type) {
case bool:
Copy link
Member

Choose a reason for hiding this comment

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

from this switch statement, i can tell that we expect bool or *metav1.Configuration to be included in this map. interestingly enough, both of these types will end up with a usable *metav1.Configuration after processing occurs in this switch.

Would it make more sense to model m.confs as map[string]*metav1.Configuration instead? when we find just the image we could insert the string key with a nil value. When we find the image and a *metav1.Configuration we can insert with a non-nil value.

that way, we can tell a couple things while using a more strict limit on the types we allow/expect that is arguably easier to read and reason about.

  1. we know we have seen an image before if the key is present
  2. we know we have a final *metav1.Configuration to use if the value is nil or not

would that work better in your opinion? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This seems easier to read and understand but with that approach we will be assigning each of the configuration we downloaded to the m.confs map.
I initially designed my approach as you described but it felt like we might use too much memory in cases where user loads too much configuration but looking at it now I also feel like a bunch of configuration yaml files wouldn't be a problem for us in the memory department if we dont have like 1000 of them. I will change this approach to your proposed one.

Copy link
Contributor Author

@enesonus enesonus Jul 12, 2024

Choose a reason for hiding this comment

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

At 75380c0 I updated the code to use your proposed logic, please let me know if there are any issues!

"name": "config-pkg",
},
"spec": map[string]interface{}{
"package": "config-dep-2:v1.3.0",
Copy link
Member

Choose a reason for hiding this comment

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

my mental model of the dependency graph is already confused by this point 😂 - would you be able to add a comment somewhere that shows a quick little ascii tree of the dependency graph used in this test?

i tried to construct it myself, but that actually confused me a bit, because i'm not sure if there are 2 separate config packages in play here. Are the 2 configuration entries in this extensions list referring to the same underlying configuration (e.g. one is the pkg and one is the meta, but for the same configuration), or to two entirely separate configurations?

Copy link
Member

Choose a reason for hiding this comment

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

it may make sense to have a 1st test case for the config pkg and 2nd test case for the config meta, so you are testing in isolation that they both work independently.

Right now, with them possibly intertwined (but i'm not sure exactly if that's the case 😇), it may be hard to tell that they are both working OK or you just happened to get all the final dependencies correct because the dependencies overlap between the two. If that was the case, one could work while the other one doesn't, yet the final result is still the same due to the overlapping dependencies.

Then in a 3rd separate case, if these config meta and config pkg are indeed separate things, it would be useful to make sure its all working when both are specified.

Copy link
Contributor Author

@enesonus enesonus Jul 12, 2024

Choose a reason for hiding this comment

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

I updated the tests with 3 cases which uses Configuration.meta and Configuration.pkg seperately and 3rd case with both with commit 75380c0.

Co-authored-by: Ezgi Demirel <ezgi@upbound.io>
Signed-off-by: Mehmet Enes <94247411+enesonus@users.noreply.github.com>
@ezgidemirel
Copy link
Member

Something interesting I noticed is that with this approach we lose the ability to validate the XR (and other types that may be defined in the local package itself). For example:

[!] could not find CRD/XRD for: aws.platform.upbound.io/v1alpha1, Kind=XEKS

Looks like we only will get the schemas for dependencies mentioned in the crossplane.yaml file and nothing that is directly in the local package being tested. What do we think of that limitation? 🤔

@jbw976 for the scenario we want to cover, the configuration is still under development. If the author wants to test the XR against the XRD, he/she can give it directly as it is to the validate command just like any other schema. The download and schema extraction logic is for improving the user experience. I don't think it's a limitation in this case.

Signed-off-by: Mehmet Enes <menes.onus@gmail.com>
Comment on lines +35 to +42
configPkg = []byte(`apiVersion: meta.pkg.crossplane.io/v1alpha1
kind: Configuration
metadata:
name: config-pkg
spec:
dependsOn:
- provider: provider-dep-1
version: "v1.3.0"
Copy link
Member

Choose a reason for hiding this comment

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

@enesonus , in the test, you're mocking this meta resource as OCI layer. However, meta resources are not real resources and there are no images for them. Therefore, packages cannot depend on a meta resource.

You can update this meta resource to a real configuration package resource to test a configuration depending on a configuration package scenario.

Copy link
Member

@jbw976 jbw976 Jul 18, 2024

Choose a reason for hiding this comment

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

The way I'm reading FetchBaseLayer(), I'm expecting the mock to return the contents of the base layer which will always have a meta pkg object for extraction in extractPackageContent(). So I thiiiink that this is correctly a meta.pkg type, but then the providerYaml and funcYaml should be consistent and also be a meta.pkg type. So I think those are the ones to update for correctness in this test?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually I reread this code and saw that we don't even return the providerYaml and funcYaml. We can simply use a real configuration.pkg like configuration-aws-network:v0.12.0 and even get rid of the mocking. About the crossplane.yaml, since all extensions gets processed and converted to *unstructured.Unstructured we can directly provide it inside the test case as *unstructured.Unstructured and do not use any files. This is because at this test we are only running addDependencies() and PrepExtensions(). These functions do not have anything to do with downloading providers and functions.

cacheDependencies() is the function which needs the information of providers and functions to download them and we dont use it in this test.

I think directly using FetchBaseLayer() is better than mocking. I will change these tests to directly use it now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You can see the changes at e74d1fc . It made the code much shorter and easier to follow IMO.

Copy link
Member

Choose a reason for hiding this comment

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

It is easier to read/follow now because it's shorter and more direct, but I do have a couple of concerns with this approach:

  • It puts a dependency from the unit test on the xpkg.upbound.io service being up and healthy. It's typically better to avoid outside service dependencies from unit tests. Do we have examples you've seen in the code base already where we are doing this (e.g. relying on the real xpkg.upbound.io)?
    • e.g., if xpkg.upbound.io can't be reached during the unit test, then it fails with something like:
 --- FAIL: TestConfigurationTypeSupport/SuccessfulConfigPkg (0.05s)
        manager_test.go:164:
            All dependencies should be successfully added from Configuration.pkg
            addDependencies(...): -want error, +got error:
              any(
            + 	e`cannot download package xpkg.upbound.io/upbound/configuration-aws-network:v0.12.0: cannot get config: reading image "xpkg.upbound.io/upbound/configuration-aws-network:v0.12.0": Get "https://xpkg.upbound.io/v2/": dial tcp: lookup xpkg.upbound.io: no such host`,
              )

So I think it's probably still more reliable to isolate our code/runtime dependencies here and mock the fetcher, so we're not relying on a live registry and we can define fake package details entirely within this test. Does that make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So your opinion is that instead of relying the test on xpkg.upbound.io service being up and healthy we should continue to use configPkg providerYaml and funcYaml with mocking and change the apiVersion of providerYaml and funcYaml to meta.pkg am I correct?

I saw some examples of xpkg.upbound.io being used [1, 2] and assumed those are using the service but checked it now without internet connection and they worked fine i.e. they dont depend on xpkg.upbound.io being healthy.

Copy link
Member

Choose a reason for hiding this comment

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

Yes sir, that feels like the best approach that will also work when we extend the implementation to handle deeper dependencies and also provider/function dependencies 🙇‍♂️

Copy link
Contributor Author

@enesonus enesonus Jul 19, 2024

Choose a reason for hiding this comment

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

I updated the tests with the approach you suggested at b7a5400 @jbw976 Thanks for the feedback!

Copy link
Member

Choose a reason for hiding this comment

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

cool @enesonus! one small thing I just noticed is that the mocked provider/function meta.pkg objects are still using the spec of their pkg counterparts. e.g. they should not have a spec.package.

I think it doesn't affect the functionality of this test because they don't have deeper dependencies, but I would still update them to be consistent. The configuration.meta.pkg looks correct, so update the provider/function meta.pkg to be similar 🤓

Probably having a comment saying which package they are for would then be useful for the reader, since that info isn't directly captured in the meta.pkg type itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh really sorry about that one. I forgot the spec.package there 😅 I removed them and added comments about package:version info at 83ce98f

@jbw976
Copy link
Member

jbw976 commented Jul 18, 2024

If the author wants to test the XR against the XRD, he/she can give it directly as it is to the validate command just like any other schema

good point @ezgidemirel, thanks for reminding me of the scope of this scenario! that sounds reasonable to me 👍

if err != nil {
return errors.Wrapf(err, "cannot download package %s", image)
}
if cfg == nil {
Copy link
Member

Choose a reason for hiding this comment

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

i really like the way this function simplified! if we don't have the config yet, we look it up, then we join the flow back and proceed the same way for all cases.

want want
}{
"SuccessfulConfigPkg": {
//config-pkg
Copy link
Member

Choose a reason for hiding this comment

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

nice, this little dependency tree diagram really helps quickly understand what the test case is setting up and testing! 💪

Signed-off-by: Mehmet Enes <menes.onus@gmail.com>
@enesonus enesonus force-pushed the config-meta-validate branch from e74d1fc to b7a5400 Compare July 19, 2024 19:12
Signed-off-by: Mehmet Enes <menes.onus@gmail.com>
Copy link
Member

@jbw976 jbw976 left a comment

Choose a reason for hiding this comment

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

Awesome, thanks @enesonus for driving this PR to completion!! 🙇‍♂️

@jbw976 jbw976 merged commit 6b8545c into crossplane:master Jul 22, 2024
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.

crossplane beta validate should support Configuration.meta / crossplane.yaml for pulling dependencies

3 participants