Skip to content

Conversation

Tomasz-Smelcerz-SAP
Copy link
Member

@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP commented Sep 15, 2025

Description

Note to reviewers: I was really trying to keep this PR as minimal as possible. There are many things to fix/improve/cleanup and many times I've seen that, but I decided not to, in order to introduce as few changes as possible.
Keep that in mind when suggesting improvements. Do we really want to make this PR even bigger? I would rather prefer to make a note in the sources and then create a follow-up PR to clean all the notes. Just remember that the work remaining in #2526 and our other efforts (like simplifying/getting rid of templatelookup.go) will probably make most of these review-fixes obsolete or unnecessary.
Up to you.

Changes proposed in this pull request:

  • Component Descriptors (CD) are now read from the OCM repository (which usually is some OCI registry)
  • To read a CD, one must provide an OCM Component Name and a version. In our current setup, the OCM Component Name is available only in a ModuleReleaseMeta for a given Module.
  • It means that with this PR the ModuleReleaseMeta becomes mandatory
  • To keep this PR minimal, I did not remove code/tests related to the previous "modes" of operation, where CD was fetched from a ModuleTemplate and ModuleReleaseMeta was, in some cases, optional. This code requires cleanup in a separate PR. Of course when the test is failing, I had to refactor it
  • An interesting case: What to do if user configures a Module for which there is no ModuleReleaseMeta? Currently, when user configures a module on a Kyma, for which there's no ModuleTemplate, the Kyma is put in Warning state. To keep it consistent, I use the same error->status resolution strategy. To be discussed.
  • Code coverage for some packages (templatelookup) decreased. This is because some test cases were disabled. This is (mostly) a legacy code that will disappear anyway. A new method of resolving ModuleTemplates along with ModuleReleaseMeta should be introduced, but this is not a part of this PR. I think it is OK to temporarily "lower the bar" - we're in the middle of a bigger refactoring here. Related issues that will introduce the necessary cleanpup/changes are listed are listed here
  • There is an open discussion that focuses on the little detail: The OCI registry URL. Previously it was taken from the static ComponentDescriptor data embedded in the ModuleTemplate. Now we have at least two different values we can use: An explicitly configured one and the one from ComponentDescriptor, after it is successfully fetched (we still need OCI Registry URL for subsequent operations, like raw-manifest fetching). Which URL to choose? Please take a look at the discussion. I decided to rely on explicitly provided value as I had no feedback that I should chose a different strategy here.

Related issue(s)
#2601

Copy link

Manifests created with 'make dry-run-control-plane' changed! Please make sure to check if changes are needed in related repositories like management-plane-charts, runtime-watcher, etc.

@Tomasz-Smelcerz-SAP
Copy link
Member Author

We are blocked here by: <internal-repo>/kyma/test-infra/issues/781 - we need all the ModuleReleaseMeta to have OCM version related attributes. In particular spec.mandatory.version

@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP force-pushed the feat/read-component-descriptor-from-oci-registry branch from f5357db to 6fca147 Compare September 24, 2025 05:38
@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP marked this pull request as ready for review September 30, 2025 06:31
@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP requested a review from a team as a code owner September 30, 2025 06:31
@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP force-pushed the feat/read-component-descriptor-from-oci-registry branch from e258230 to 9d9eb17 Compare September 30, 2025 07:21
@lindnerby lindnerby linked an issue Sep 30, 2025 that may be closed by this pull request
1 task
@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP force-pushed the feat/read-component-descriptor-from-oci-registry branch 3 times, most recently from 0be33b5 to 3be024d Compare September 30, 2025 13:29
@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP changed the title Feat/read component descriptor from oci registry feat: Read component descriptor from oci registry Sep 30, 2025
@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP force-pushed the feat/read-component-descriptor-from-oci-registry branch 3 times, most recently from 071775d to 6aee460 Compare October 1, 2025 08:47
Copy link

github-actions bot commented Oct 1, 2025

⚠️ Pipeline-related file changes detected! Please review if related updates (e.g. manifest generation or workflow adjustments) are required.

@ruanxin ruanxin added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Oct 1, 2025
@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP force-pushed the feat/read-component-descriptor-from-oci-registry branch 3 times, most recently from d3509af to 5ac7c15 Compare October 7, 2025 18:52
Copy link

github-actions bot commented Oct 7, 2025

Manifests created with 'make dry-run-control-plane' changed! Please make sure to check if changes are needed in related repositories like management-plane-charts, runtime-watcher, etc.

@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP force-pushed the feat/read-component-descriptor-from-oci-registry branch from 1a7339b to 4552c9a Compare October 7, 2025 20:12
@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP force-pushed the feat/read-component-descriptor-from-oci-registry branch 3 times, most recently from 916d93f to e3b1e83 Compare October 8, 2025 23:49
}

func GetModuleTemplate(ctx context.Context,
func GetModuleTemplateInfo(ctx context.Context,
Copy link
Member Author

Choose a reason for hiding this comment

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

Review Note: I changed the return type of this function for a reason.
Previously the returned ModuleTemplate instance was enough to also get the OCM descriptor, as it was embedded in the ModuleTemplate.
For an example, look how the "ReadModuleVersionFromModuleTemplate" function (now renamed to "GetOCMVersionForModule" was making use of this data (before current changes).

Now, in order to find an OCM descriptor, or at least an OCM identitfier for the given ModuleTemplate, we cannot rely on the ModuleTemplate instance alone.
But there are some callers of this function that still need this information, for example: the mentioned "ReadModuleVersionFromModuleTemplate" function.

What can we do to support these callers?
Please note that although ModuleTemplate does not provide ComponentDescriptor anymore (or rather we don't want to use it anymore), the ModuleTemplateInfo object contains the data we need: the ModuleTemplateInfo implements the OCMIdentity interface.
And the good news is: we have a ready to use ModuleTemplateInfo instance here.
Thus it's a natural choice to make use of this instance (as we have it anyway).
The change to multiple return values in signature is intentional: it forces the compiler to return errors in all places where this function was used - and because of that, ensures that I fixed the code in all of these places.

@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Oct 14, 2025
Copy link
Contributor

@c-pius c-pius left a comment

Choose a reason for hiding this comment

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

Intermediate review. Have not looked at test files and other chore like .github yet.

continue
}

if template.ComponentIdentity == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the scenario where this is not nil? As far as I can see, the only other place where we write this is the "by module release meta lookup strategy" which is not used for mandatory modules.

Comment on lines +36 to +41
// Convenience interface to get the OCM identity of a component from objects
// that already have all required data.
// Then we don't have to create intermediate variables of type ocmidentity.Component.
type OCMIProvider interface {
GetOCMIdentity() (*ocmidentity.Component, error)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Wondering if the ModuleTemplateInfo could also directly implement the ocmidentity.Component interface (Name(), Version()). Then we wouldn't need this interface and could directly pass the ModuleTemplateInfo. But the function names are rather generic, so also not too sure if this is a good idea. But since we own the ocmidentity.Component interface, we could also make the methods more specific (GetOcmComponentName(), GetOcmComponentVersion()).

But if we keep the interface, could we move the type up to before the constructor? I think it is easier to read if the file is ordered like "types -> vars -> constructor -> funcs"

if desc == nil {
return nil, ErrDescriptorNil
}
func (c *CachedDescriptorProvider) Add(ocmi ocmidentity.Component) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Good catch actually. I am a bit confused about the usage and exact implementation of the provider though. In regular.go.

For the same module (ocmi), we always first add (updateCache: true), and then get (updateCache:false). Why not only doing a get that is updating the cache in the beginning and get rid of Add? Also, I think the updateCache bool is also misleading. I would expect that this forces the cache to update. But instead, it is only controlling if the cache is updated AFTER not finding the descriptor in the cache. Not sure what the purpose is for that?

for _, moduleInfo := range FetchModuleInfo(kyma) {
       // ...
       if err := t.descriptorProvider.Add(*ocmi); err != nil {
			templateInfo.Err = fmt.Errorf("failed to get descriptor: %w", err)
			templates[moduleInfo.Name] = &templateInfo
			continue
		}
		for i := range kyma.Status.Modules {
			moduleStatus := &kyma.Status.Modules[i]
			if moduleMatch(moduleStatus, moduleInfo.Name) {
				descriptor, err := t.descriptorProvider.GetDescriptor(*ocmi)
				if err != nil {
					msg := "could not handle channel skew as descriptor from template cannot be fetched"
					templateInfo.Err = fmt.Errorf("%w: %s", ErrTemplateUpdateNotAllowed, msg)
					continue
				}
				markInvalidSkewUpdate(ctx, &templateInfo, moduleStatus, descriptor.Version)
			}
		}

Comment on lines +36 to +39
type OCIRepository interface {
GetConfigFile(ctx context.Context, name, tag string) ([]byte, error)
PullLayer(ctx context.Context, name, tag, digest string) (containerregistryv1.Layer, error)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

How about passing the ocmidentity.Component directly instead of separated name and tag (version)

Copy link
Contributor

Choose a reason for hiding this comment

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

Digest could also use the digest type. In the call below, the typecast should not be needed anyway.

Copy link
Member Author

@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP Oct 15, 2025

Choose a reason for hiding this comment

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

How about passing the ocmidentity.Component directly instead of separated name and tag (version)

I was thinking about it, but it is "polluting" OCIRepository with an abstraction that doesn't belong there (OCM is not something that OCI Repository "knows" about). We could have a dedicated type that represents <name, tag, digest> and do the conversion. Because this repository is very simple, I decided for just strings. But I am always in favor of having a dedicated type.
Should I introduce it?
Or we could use the fact that the Component type offers methods: Name() and Version() - we could accept an interface that matches these methods.

Comment on lines 119 to 123
// SimplifiedLayer represents only necessary part of containerregistryv1.Layer to simplify testing.
type SimplifiedLayer interface {
Digest() (containerregistryv1.Hash, error)
Uncompressed() (io.ReadCloser, error)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This should not be needed. We can have func extractFile(ioh extractFileIOHelper, layer containerregistryv1.Layer, fileName string) ([]byte, error) { with mock layer

type mockLayer struct {
	containerregistryv1.Layer

	errOnUncompressed error
	errOnDigest       error
}

Needed Uncompressed() and Digest() are overriden. Rest are implicitly exposed but will not work since we didn't provide an actual Layer

Copy link
Member Author

@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP Oct 15, 2025

Choose a reason for hiding this comment

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

Thanks for catching that: Indeed SimplifiedLayer shouldn't be exported, it should be a package internal type - then we don't pollute our API with internals.
After that change I'd say it's a matter of taste. I prefer explicit typing (function is given only the the abstraction it absolutely needs), even if it means introducing a new interface. If it is unexported, nobody knows cares, and performance-wise it is exactly the same I think?
I somehow don't like the fact that mockLayer will now panic: runtime error: invalid memory address or nil pointer dereference when invoked with a non-implemented method, even though we're not doing that now - but who knows the future?
But maybe it's just my code-related OCD :)
I will change that as you suggested, but If we'll ever see panic here, then I can say: ‘See? I told you!’ :)

Comment on lines +159 to +169
type extractFileIOHelper interface {
ReadAll(r io.Reader) ([]byte, error) // Reads all data from the reader
}

type defaultExtractFileIOHelper struct {
readAllFunc func(r io.Reader) ([]byte, error)
}

func (d *defaultExtractFileIOHelper) ReadAll(r io.Reader) ([]byte, error) {
return d.readAllFunc(r)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

same comment as for the wrapper in ocirepo.go. If we don't decide to go that we, I would at least name it consistently with Wrapper or Helper because eventually it is the same thing.

Comment on lines +207 to +223
// untarIoHelper abstracts the methods of tar.Reader and io package used in unTar function, for better testability.
type untarIoHelper interface {
Next() (*tar.Header, error) // tar.Reader.Next()
CopyN(dst io.Writer, n int64) (written int64, err error) // modified io.CopyN()
}

type defaultUntarIOHelper struct {
tarReader *tar.Reader
}

func (d *defaultUntarIOHelper) Next() (*tar.Header, error) {
return d.tarReader.Next() //nolint:wrapcheck // this helper should be transparent
}

func (d *defaultUntarIOHelper) CopyN(dst io.Writer, n int64) (int64, error) {
return io.CopyN(dst, d.tarReader, n) //nolint:wrapcheck // this helper should be transparent
}
Copy link
Contributor

Choose a reason for hiding this comment

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

same comment as for the wrapper in ocirepo.go. If we don't decide to go that we, I would at least name it consistently with Wrapper or Helper because eventually it is the same thing.

Comment on lines +36 to 42
// Implements provider.OCMIProvider interface.
func (m ModuleTemplateInfo) GetOCMIdentity() (*ocmidentity.Component, error) {
if m.ComponentIdentity == nil {
return nil, fmt.Errorf("%w for module template %s", ErrNoIdentity, m.Name)
}
return m.ComponentIdentity, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

See comment in provider.go

Comment on lines +228 to +236
ociRegistryHost := getOciRegistryHost(mgr.GetConfig(), flagVar, logger)
var insecure bool

if noSchemeRef, found := strings.CutPrefix(ociRegistryHost, "http://"); found {
insecure = true
ociRegistryHost = noSchemeRef
} else if noSchemeRef, found := strings.CutPrefix(ociRegistryHost, "https://"); found {
ociRegistryHost = noSchemeRef
}
Copy link
Contributor

@ruanxin ruanxin Oct 15, 2025

Choose a reason for hiding this comment

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

this whole block can be getOciRegistryHost, and I would design this ociRegistryHost as url.URL type, you can use url.Parse(rawURL string), then you don't need to deal with cutting prefix, validating anymore. And you can use Scheme field to determine if it's insecure, so there is no need to claim insecure here and pass to NewRepository

Copy link
Member Author

@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP Oct 15, 2025

Choose a reason for hiding this comment

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

I know that, but this is the existing code - and it returns a string, not a url.URL...
Of course I could refactor that and tens of other places in the code. But read this, I've just added this disclaimer as the first note in the PR description:

Note to reviewers: I was really trying to keep this PR as minimal as possible. There are many things to fix/improve/cleanup and many times I've seen that, but I decided not to, in order to introduce as few changes as possible.
Keep that in mind when suggesting improvements. Do we really want to make this PR even bigger? I would rather prefer to make a note in the sources and then create a follow-up PR to clean all the notes. Just remember that the work remaining in https://github.com/kyma-project/lifecycle-manager/issues/2526 and our other efforts (like simplifying/getting rid of templatelookup.go) will probably make most of these review-fixes obsolete or unnecessary.
Up to you.

Copy link
Contributor

Choose a reason for hiding this comment

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

That’s totally fine — the review comments are meant to highlight potential issues, not necessarily to be fixed right away. If you’d prefer not to address them in this PR, feel free to create a follow-up issue instead.

At the same time, not mentioning these code smells would be my oversight. Continuously improving our existing codebase should always be our goal.

keyChainLookup spec.KeyChainLookup
hostref string
insecure bool
cWrapper craneWrapper // in runtime delegates to crane package functions
Copy link
Contributor

Choose a reason for hiding this comment

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

I will rename this interface more generic to reflect it's usage, it's basically to manage container images, right? why not something like ContainerImageManager? crane is just a specific implementation.

Copy link
Member Author

Choose a reason for hiding this comment

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

The answer is: It uses types from crane library, is strictly tied to it. It is not generic. I can rename it, but these types will still point out to the crane library. It will be misleading then: The type name will suggest a generic abstraction, but the interface methods will force you to have just a single, crane-based implementation.

Copy link
Contributor

Choose a reason for hiding this comment

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

I disagree with your answer. An interface should always be designed to be generic, regardless of the number of existing implementations. The point of having an interface is not to describe the current dependency (in this case, crane), but to define a stable abstraction boundary between your business logic and the external library.

The consumer of this interface shouldn’t need to know or care what crane is; it should only rely on the capability it provides, in this case, it's "pulling a container image." However, what you are trying to do now is make the interface even deeply coupled with the lower-level details, which is certainly against the Dependency Inversion Principle (DIP): High-level modules shouldn’t depend on low-level details (like the crane library). Both should depend on abstractions.

keyChainLookup: kcl,
hostref: hostref,
insecure: insecure,
cWrapper: &defaultCraneWrapper{},
Copy link
Contributor

@ruanxin ruanxin Oct 15, 2025

Choose a reason for hiding this comment

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

We are agreed in ADR to introduce dependencies via Constructor Injection, but not using this implecity way, it hardcoded with &defaultCraneWrapper{}

Copy link
Member Author

@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP Oct 15, 2025

Choose a reason for hiding this comment

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

I don't agree.

In the ADR there's a link to a poorly written article, that states:
[Dependency Inversion] is a principle whose name is often misused interchangeable with Dependency Injection even it is not the same.
and:

The principle consists of two core concepts: high-level modules should not depend on low-level modules, and both should depend on abstractions. 

First: We don't have any "modules" (plural) here - there's just one "module". The craneWrapper is an unexported, (package-private) interface created to facilitate testing. The purpose? To be able to use "real" crane library in production, and a mock implementation for testing.
Yes, we are depending on the crane library, but no: we don't want to have user-injected "concrete implementations" here.
Contrary to the examples given in the mentioned article, this abstraction is not external, like a ConcreteUserRepository, or SqlClient, or something very common in our code: client.Client. I agree that such abstractions, external to the package, should be managed by Dependency Injection, either by using Constructor functions or in some other way. We agreed on using Constructors, fine.

But this abstraction is totally internal to the package.
None of the arguments given in the article applies:

  • Changing the dependency is not a concern here. We'll rather have a different repository implementation if we want to use something else than the crane library. And it will be defined in a different package.
  • Testing is not a concern: In Go (unlike Java in the article) we can test this without any problems as you can see. This is officially supported and promoted way of testing things in Go.

That's why I think we shouldn't expose craneWrapper as a constructor parameter: This would be an abstraction leak: We'd be leaking internal details of which library is used to communicate with the OCI Registry.
In addition:

  • Users of the package should not know about craneWrapper - because they don't need this knowledge to use the API.
  • Users of the package should not be forced to configure the constructor with a craneWrapper argument even though it doesn't bring any value to them. This implementation is fixed, there are no other "concrete implementations" to choose from. Formal parameters exists in programming languages to give users the freedom of providing different actual arguments. There is no such choice in that case.

In summary, exposing this in the constructor violates the following rules/practices:

  • Information Hiding (we're exposing an internal testing-related facility to the world)
  • Law of Demeter (Principle of Least Knowledge)
  • Encapsulation of the package

And again, we're not violating the "Dependency Inversion" because we don't have different modules (packages) here.

// RepositoryReader provides basic support to read data from OCI repositories.
type RepositoryReader struct {
keyChainLookup spec.KeyChainLookup
hostref string
Copy link
Contributor

Choose a reason for hiding this comment

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

Each part of URL have a standard name, this is just host, I never heard of hostref.

return nil, fmt.Errorf("%w: %q", ErrNoProtocolScheme, hostref)
}

if strings.HasPrefix(hostref, "/") {
Copy link
Contributor

@ruanxin ruanxin Oct 15, 2025

Choose a reason for hiding this comment

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

Please check my comment here https://github.com/kyma-project/lifecycle-manager/pull/2736/files#r2432603480, if you already pass hostref as url.URL, each section of the url address belongs to a specific field (Scheme, Host, Path ...), you don't need to validate host anymore.

}

func (s *RepositoryReader) toImageRef(name, tag string) string {
hostPath := path.Join(s.hostref, "component-descriptors", name)
Copy link
Contributor

Choose a reason for hiding this comment

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

We have const value defined DefaultRepoSubdirectory

// Note: Here we're just adding OCM identity information.
// It doesn't change how the Mandatory Modules are selected for installation:
// we still take the latest version of every ModuleTemplate which is marked as mandatory.
// The switch to the logic based on ModuleReleaseMeta will be done in a follow-up PR.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please reference the related issue regarding switch to the logic based on ModuleReleaseMeta here. will be done in a follow-up PR doesn't bring documentation value.

@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP force-pushed the feat/read-component-descriptor-from-oci-registry branch from 6c9898e to 99ce9b3 Compare October 15, 2025 14:43
func NewCachedDescriptorProvider(service DescriptorService) *CachedDescriptorProvider {
return &CachedDescriptorProvider{
DescriptorCache: cache.NewDescriptorCache(),
descriptorCache: cache.NewDescriptorCache(),
Copy link
Contributor

Choose a reason for hiding this comment

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

This descriptorCache should also be injected via constructor based on our ADR

func (c *CachedDescriptorProvider) Add(template *v1beta2.ModuleTemplate) error {
if template == nil {
return ErrTemplateNil
func (c *CachedDescriptorProvider) getDescriptor(ocmi ocmidentity.Component, updateCache bool) (
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it's good approach mix the update cache together with get, they are seperate logic. And you only need update cache in Add, there is no need introduce such flag, also vailate single responsiblity.

Copy link
Member Author

@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP Oct 16, 2025

Choose a reason for hiding this comment

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

OK, this is an internal function... The code used to be in two functions: Add and Get, but I realized the code is almost identical. So I thought the reviewers will point out, that the code can be shared...
But OK, let's duplicate it again. no problem for me.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

// where a single descriptor is used for multiple test cases with slightly different module names.
// Defining this fake here has the advantage of using the same internal
// deserialization logic as the real service, which makes this fake a bit more "real".
type FakeService struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

This one should exist under _test package specifically.

@@ -0,0 +1,123 @@
package componentdescriptor
Copy link
Contributor

Choose a reason for hiding this comment

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

rename to componentdescriptor_test

@@ -0,0 +1,74 @@
package componentdescriptor
Copy link
Contributor

@ruanxin ruanxin Oct 15, 2025

Choose a reason for hiding this comment

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

rename to componentdescriptor_test, and refactor those internal function you are testing as suggestion given here: https://github.com/kyma-project/lifecycle-manager/pull/2736/files#r2432968414

"github.com/stretchr/testify/require"
)

func TestUnTar(t *testing.T) {
Copy link
Contributor

@ruanxin ruanxin Oct 15, 2025

Choose a reason for hiding this comment

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

This internal function is worth extracting into a dedicated struct member function, and inject into componentdescriptor Service. Once extracted, you can make it public and test it through its public method instead of testing this internal function directly.

Copy link
Member Author

@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP Oct 15, 2025

Choose a reason for hiding this comment

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

Why would I do that? Nobody else (no package API user) wants to use that function and making it public increases package API surface for no reason.
I could extract this to a separate package instead, but then: What's the difference between that and just making the function public? I am still exposing a "private" thing via public API (and to make things worse I introduce a new package for that), although nobody asks for it.
That's a bad practice, package APIs should be minimal and hiding implementation details is exactly what packages are for.
BTW: Function doesn't have to be exported to be testable in Go. If in doubt, please see this official go docs
If the test file is in the same package, it may refer to unexported identifiers within the package, as in this example[...]
This is supported and promoted way of writing tests in Go.

*builder.NewModuleTemplateBuilder().
WithName(fmt.Sprintf("%s-%s", module.Name, testModule.Version)).
WithModuleName(module.Name).
WithChannel(module.Channel).
Copy link
Contributor

Choose a reason for hiding this comment

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

Since you already refactor this part, please do not assign channel to ModuleTemplate anymore, channel is in MRM now.

Copy link
Member Author

@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP Oct 15, 2025

Choose a reason for hiding this comment

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

I did not touch "WithChannel" on purpose, because it's a journey down the rabbit hole of refactoring. Removing that means removing methods, errors, etc., which ultimately leads to doubling the work others are doing/planning in PR related to ModuleTemplate resolution strategies cleanup (by_channel_strategy). I prefer not to touch it and leave this cleanup to the PR that is addressing this specific concern.


// Component uniquely identifies an OCM Component.
// See: https://ocm.software/docs/overview/important-terms/#component-identity
type Component struct {
Copy link
Member

Choose a reason for hiding this comment

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

ComponentId would be even more fitting as the structs name to reflect what it is.

Copy link
Member

Choose a reason for hiding this comment

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

Usade and function during the code even call this differently already:
OCMIdentity
Would be good to align this and settle for a name, to talk about one thing from the beginning.

}

// MustNew is a convenience constructor that panics if name or version are not provided.
func MustNew(name, version string) *Component {
Copy link
Member

Choose a reason for hiding this comment

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

Probably this is not needed. It's even dangerous because someone might use it in production code.
Looks like this is anyways only needed in tests so any convenience function can live in test_utils.

}, nil
}

func (c *Component) Name() string {
Copy link
Member

Choose a reason for hiding this comment

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

Since these are only read methods (similar to getters) they can even have a value receiver instead of pointer to further indicate that:
func (c Component) Name() string {


type DescriptorKey string

func GenerateDescriptorKey(ocmi ocmidentity.Component) DescriptorKey {
Copy link
Member

Choose a reason for hiding this comment

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

Did not find a better or unused place to put this comment 😄
Once settled for a consistent name for ocmidentity.Component you should also adapt the the parameter names throughout the whole PR. So either: ocmId or componentId or ocmCompId or ocmIdentity, whatever the agreed name of the domain entity will be then.

return nil, fmt.Errorf("%w for %s", ErrLayerNil, commonErrMsg(ocmi))
}

compDescLayerDigest := ocmArtifactConfig.ComponentDescriptorLayer.Digest
Copy link
Member

Choose a reason for hiding this comment

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

Until here all that is done is needed to get the digest. You can wrap everything in a private method getDescriptorLayerDigest(*) to indicate that. Also allows for a comment on the func to describe details.

ReadAll(r io.Reader) ([]byte, error) // Reads all data from the reader
}

type defaultExtractFileIOHelper struct {
Copy link
Member

Choose a reason for hiding this comment

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

Can you explain what is the reasoning behind these internal interface and immediate struct implementations?
Can you think about using dependency injection for these helper functionalities?

Copy link
Member

Choose a reason for hiding this comment

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

As I see it the untarIoHelper as well as the defaultExtractFileIOHelper can be injected dependencies?

- name: Create and apply ModuleReleaseMeta from the template-operator repo
working-directory: template-operator
if: ${{ matrix.e2e-test == 'kyma-metrics' ||
matrix.e2e-test == 'non-blocking-deletion' ||
Copy link
Member

Choose a reason for hiding this comment

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

Quick info on why this testcase is not important anymore? I don't know the details.

ocmi := ocmidentity.MustNew(tc.moduleName, tc.moduleVersion)
got := cache.GenerateDescriptorKey(*ocmi)
assert.Equal(t, tc.want, string(got))
})
Copy link
Member

Choose a reason for hiding this comment

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

Just to align some things here:

  • DescriptorKey could also get its own file if the test gets it? Or incorporate this test as well in cache_test.go.
  • If there is only one testcase there is no need for a table driven structure. So either we come up with more testcases here or have just one plain test.

@@ -0,0 +1,6 @@
package oci

// compiled only when running tests.
Copy link
Member

Choose a reason for hiding this comment

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

Why introduce this indirection and not create a separate pkg for the wrapper isolated with its tests.
And then inject the wrapper dependency for consumers as usual.

Copy link
Member Author

@Tomasz-Smelcerz-SAP Tomasz-Smelcerz-SAP Oct 16, 2025

Choose a reason for hiding this comment

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

Why introduce another package "B" just to test package "A"?
Packages should have a purpose. In this case, package "A" - internal/repository/oci can "handle itself" very well, we can write all the tests we need without introducing new public abstractions.
I guess "do not multiply entities beyond necessity" is still the correct approach?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Read component descriptor from oci registry

5 participants