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

Support camelcase matchLabels and matchExpressions in TA config #3418

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

davidhaja
Copy link
Contributor

Description:

Currently TA cannot parse camel case matchLabels and matchExpressions fields due to the limitation of gopkg.in/yaml.v2 package and due to the lack of the yaml tag in metav1.LabelSelector struct definition.
The github.com/goccy/go-yaml parses the camel case labelselector fields by default and provides the possibility to parse a specific route in a yaml string. Relying on this feature the TA can unmarshal the only lowercase version of these fields, and then merge together with the originally unmarshalled config.
Therefore both the camel case and the only lowercase fields of the LabelSelector will be handled.

Link to tracking Issue(s): 3350

Testing:
Provided test configs with camelcase matchLabels and matchExpressions

Documentation:
N/A

@davidhaja davidhaja marked this pull request as ready for review November 4, 2024 08:39
@davidhaja davidhaja requested a review from a team as a code owner November 4, 2024 08:39
@jaronoff97
Copy link
Contributor

@davidhaja did you try to use mapstructure? I wonder if that would work more effectively than trying to implement some custom support here. Also, because LabelSelector has json tags, we may be able to use this unmarshaller which does a conversion from yaml -> json and then uses json decoders

@davidhaja
Copy link
Contributor Author

@davidhaja did you try to use mapstructure? I wonder if that would work more effectively than trying to implement some custom support here. Also, because LabelSelector has json tags, we may be able to use this unmarshaller which does a conversion from yaml -> json and then uses json decoders

@jaronoff97 I've tried this package.
I wrote a comment in the original issue about what I've tried before I implemented the change: #3350 (comment)

Copying it here:

The way I see is either we try to support the flexible/case insensitive unmarshalling in TA, or we change both the marshalling and unmarshalling to follow (and require) the k8s camel case convention. The latter one is a breaking change, which I believe requires more effort. (Although probably results in cleaner logic, code as well) In the submitted PR I've made some changes that implement the former option.

I've tried multiple yaml packages, but none of them supports case insensitive parsing of YAML files.

I ended up with github.com/goccy/go-yaml because of its feature set. I've described the feature that my change is using for providing case insensitive parsing.

@jaronoff97
Copy link
Contributor

Ah thank you, and did mapstructure not work at all?

@davidhaja
Copy link
Contributor Author

Ah thank you, and did mapstructure not work at all?

I haven't used mapstructure before your comment. :)
I tried it and could achieve the same functionality with less (and hopefully cleaner) code.
Thanks for the tip.

Comment on lines 153 to 182
func StringToModelDurationHookFunc() mapstructure.DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{},
) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
if t != reflect.TypeOf(model.Duration(5)) {
return data, nil
}

// Convert it by parsing
return time.ParseDuration(data.(string))
}
}

func decodeSubConfig(t interface{}, dc mapstructure.DecoderConfig) error {
dec, decError := mapstructure.NewDecoder(&dc)
if decError != nil {
return decError
}
if err := dec.Decode(t); err != nil {
return err
}
return nil
}

func flexibleUnmarshal(yamlFile []byte, cfg *Config) error {
Copy link
Contributor

@swiatekm swiatekm Nov 5, 2024

Choose a reason for hiding this comment

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

We should add docstrings to these functions, as well as a longer comment explaining why they exist.

Comment on lines 210 to 217
if err = yaml.Unmarshal(yamlFile, cfg); err != nil {
return fmt.Errorf("error unmarshaling YAML: %w", err)
}

if err := flexibleUnmarshal(yamlFile, cfg); err != nil {
return err
}

Copy link
Contributor

Choose a reason for hiding this comment

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

We should add a comment here explaining why both these statements are necessary. Incidentally, what we're doing here also means that if someone were to use both versions, the camel case version wins.

@nicolastakashi
Copy link
Contributor

Hey @davidhaja
Thanks for working on that.
I'm a bit concerned about "Two Unmarshal", I'm wondering if in this use case would be simpler just implement a custom Unmarshal function to the entire object.

wdyt?

@nicolastakashi
Copy link
Contributor

@davidhaja
Copy link
Contributor Author

@davidhaja check how we're doing that on the prometheus receiver: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/770befbc7a5e4f436b67e7bb9ecf42bf3c5e7053/receiver/prometheusreceiver/targetallocator/config.go#L31C6-L31C22

I think we can have a similar approach, wdyt?

I will check this out.
Thank you! :)

@davidhaja
Copy link
Contributor Author

@nicolastakashi
Sorry for the delay.
I have reviewed the suggestion you provided and I can see the similarities.
Although I believe I found an easier solution to support the camel case version of these fields in the TA config.

In my latest commit, I have implemented a solution where the camel case matchExpressions and matchLabels are converted to lowercase within the loaded byte array. Following this, we call the same YAML unmarshaller that we used previously.

The regular expression is employed to identify only those matchExpressions and matchLabels that define a map, ensuring they are not mistakenly matched within other strings (e.g., as part of a string value). While I believe the likelihood of this occurring is minimal, I have retained this precaution to allow flexibility for users.

Please let me know if you consider this approach to be a viable solution.
@jaronoff97 @swiatekm

@swiatekm
Copy link
Contributor

@davidhaja I'm not comfortable with a solution based on regular expressions. There's a bunch of places in the configuration where arbitrary strings can appear, so any substitutions we make should take the file structure into consideration.

If you want to go this way, I'd recommend unmarshalling the raw config to a map, making the changes, marshalling it back, and then unmarshalling into the final config struct. More wasteful, but safer.

@davidhaja
Copy link
Contributor Author

@swiatekm
Agree. Your concerns are totally valid.
I've implemented a new version, which relies on mapstructure decoding and uses some custom decode hook functions.

Please let me know what you think about this change.

cc @nicolastakashi @jaronoff97

@swiatekm swiatekm self-requested a review January 15, 2025 17:14
Copy link
Contributor

@swiatekm swiatekm left a comment

Choose a reason for hiding this comment

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

This looks much better to me. A few relatively minor nitpicks left, and we should be good to go.

go.mod Outdated Show resolved Hide resolved
cmd/otel-allocator/config/config.go Show resolved Hide resolved
@swiatekm swiatekm requested a review from jaronoff97 January 16, 2025 14:17
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.

Collector Instances Not Discovered Due to Case Sensitivity in matchLabels
4 participants