Skip to content
This repository has been archived by the owner on Jan 13, 2021. It is now read-only.

Refactor dependsOn into References #262

Merged
merged 4 commits into from
Apr 5, 2018
Merged

Refactor dependsOn into References #262

merged 4 commits into from
Apr 5, 2018

Conversation

wryun
Copy link
Contributor

@wryun wryun commented Mar 21, 2018

See #233 for some discussion. DO NOT MERGE - need to update doco if we agree. Also, integration-test may be broken (unclear what's happening on first glance).

Why we want this:

Why we don't want this:

  • verbosity for simple cases
  • we 'hack' a simple dependsOn (without references), which looks a bit ugly (maybe?). Could instead have a nested structure (e.g. resource then a list of 'names' that we extract from it)
  • not backwards compatible (will need to change voyager once we merge)
  • there is a nicer solution we haven't considered


Spec ResourceSpec `json:"spec"`
}

// +k8s:deepcopy-gen=true
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this needed given that there is a method for it below?

Copy link
Contributor

Choose a reason for hiding this comment

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

I would say the other way around. Is there a reason for manually defining a DeepCopyInto method, if it should be generated based on this annotation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's what's happening here.

for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
copy(*out, *in)
Copy link
Contributor

Choose a reason for hiding this comment

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

ARGHH #216

if !st.processedResources[dependency].isReady() {
notReadyDependencies = append(notReadyDependencies, dependency)
// No len here because dependencies can occur more than once in reference list
notReadyDependenciesSet := make(map[smith_v1.ResourceName]bool)
Copy link
Contributor

Choose a reason for hiding this comment

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

The convention is to use struct{} as value for sets. This makes it more explicit that you don't care about the value.

)

var (
reference = regexp.MustCompile(`^\{\{.+}}$`)
reference = regexp.MustCompile(`(?s)^(!+)\{(.+)}$`)
Copy link
Contributor

Choose a reason for hiding this comment

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

For the reader:
https://github.com/google/re2/wiki/Syntax
(?s) - let . match \n (default false)

func NewExamplesSpec(references []smith_v1.Reference) (*SpecProcessor, error) {
variables, err := resolveAllReferences(references, func(reference smith_v1.Reference) (interface{}, error) {
if reference.Example == nil {
return nil, errors.WithStack(&noExampleError{reference.Name})
Copy link
Contributor

Choose a reason for hiding this comment

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

Prefer specifying field names explicitly even if there is only one field. If you later refactor - add a field of the same type - this will not break the compilation but become a bug silently.

for _, reference := range references {
// Don't 'resolve' nameless references - they're just being
// used to cause dependencies.
if reference.Name != "" {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd unindent the happy path and rewrite this as if reference.Name == "" { continue }

smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1"
"github.com/atlassian/smith/pkg/resources"
"github.com/pkg/errors"
"go.uber.org/multierr"
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we use some multierr package from client-go somewhere already. Lets pick one =)

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't find it...

Copy link
Contributor

Choose a reason for hiding this comment

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

k8s.io/apimachinery/pkg/util/errors/NewAggregate()

if err != nil {
return nil, errors.Wrapf(err, "invalid reference at \"%s\"", strings.Join(path, "."))
// This means we have multiple '!' at the start, so we're escaping.
if len(match[1]) > 1 {
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 ignore pairs of ! and then process the rest if there is an unmatched !, right? This can even be done with regex - match 0+ pairs and then expect an !. Hm, but then those pairs need to be transformed into single !.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also please add tests for escaping. And we should export a function to escape a string and another one to escape a whole object. Also maybe add a flag to each resource to tell smith whether to look for references in this resource or not. Because the choice for programmatic construction is between 1) escape each one even if it does not contain references by construction 2) set a flag marking the resource as not containing references. WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree re:tests. I would like to see examples of such escaping format.

Copy link
Contributor Author

@wryun wryun Mar 26, 2018

Choose a reason for hiding this comment

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

Sorry, I thought I added tests.
I don't see a problem with this method of escaping though - it should just work, aside from the programmatic construction case... my preference here, rather than a flag, would be to look for references IFF there are references (i.e. if sp.variables is non-empty).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(yes, it's a little magical, but it keeps the base case clean)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On further thought, I think the current behaviour is the correct one. Programmatic users should just have to deal with escaping (it's relatively straightforward, as are the error cases... we also have no cases currently).

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 will add the functions to escape strings/objects in a separate PR?

Copy link
Contributor

@nilebox nilebox left a comment

Choose a reason for hiding this comment

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

Overall I like the direction of this PR.
See the comments.


Spec ResourceSpec `json:"spec"`
}

// +k8s:deepcopy-gen=true
Copy link
Contributor

Choose a reason for hiding this comment

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

I would say the other way around. Is there a reason for manually defining a DeepCopyInto method, if it should be generated based on this annotation?

for _, dependency := range res.DependsOn {
if !st.processedResources[dependency].isReady() {
notReadyDependencies = append(notReadyDependencies, dependency)
// No len here because dependencies can occur more than once in reference list
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 this is a justification for not passing an initial capacity at all.
you can easily set more key-value pairs than initially allocated, i.e. using len(res.References) is actually fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

? But then I over-allocate, potentially dramatically?
(it's not under-allocation that will happen, it's over-allocation)

Copy link
Contributor

Choose a reason for hiding this comment

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

ah I see. I doubt there is a huge memory footprint for map[string]struct{}.
And also I don't see any reason for a dependency occuring more than once in reference list.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(it will occur more than once because of the path problem)

}
}
notReadyDependencies := make([]smith_v1.ResourceName, 0, len(notReadyDependenciesSet))
Copy link
Contributor

Choose a reason for hiding this comment

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

you can allocate a fixed size array instead here:

notReadyDependencies := make([]smith_v1.ResourceName, len(notReadyDependenciesSet))
i := 0
for resourceName, _ := range notReadyDependenciesSet {
	notReadyDependencies[i] = resourceName
        i++
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Isn't this equivalent, just more verbose and error prone?

Copy link
Contributor Author

@wryun wryun Mar 26, 2018

Choose a reason for hiding this comment

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

(maybe append is slightly slower? You'd think it would be optimised away. But I'm using the same array...)

Copy link
Contributor

Choose a reason for hiding this comment

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

I think append is inherently safer and reads nicer =)

}
}
notReadyDependencies := make([]smith_v1.ResourceName, 0, len(notReadyDependenciesSet))
for resourceName, _ := range notReadyDependenciesSet {
Copy link
Contributor

Choose a reason for hiding this comment

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

_ is redundant here. Golang magic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Heh, I find it ugly to leave out. Ok, will change.

@@ -69,8 +69,10 @@ func TestWorkflow(t *testing.T) {
Spec: smith_v1.BundleSpec{
Copy link
Contributor

Choose a reason for hiding this comment

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

(for myself and other reviewers)
equivalent in YAML:

spec:
  resources:
  - name: config1res
    references:
    - resource: secret2res
    spec:
      object: ...

Copy link
Contributor

Choose a reason for hiding this comment

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

@wryun if I understand correctly, such minimal reference is only suitable for creation ordering, and not for injecting values from the other objects right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. You need to specify name/path if you want an actual value.

ar := make(map[smith_v1.ReferenceName]interface{}, len(references))
var finalerr error
for _, reference := range references {
// Don't 'resolve' nameless references - they're just being
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if I like the idea of this behavior depending on whether name is defined or not. See comments above about name vs resource.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd rather remove name or make it optional.
And we can resolve all references without checking for name != "", this optimization seems redundant to me.

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'm not sure what you mean here. Maybe talk in person.

Copy link
Contributor

Choose a reason for hiding this comment

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

TL;DR just remove this block:

		// used to cause dependencies.
		if reference.Name == "" {
			continue
		}

and I propose to delete the name field completely.

if err != nil {
return nil, errors.Wrapf(err, "invalid reference at \"%s\"", strings.Join(path, "."))
// This means we have multiple '!' at the start, so we're escaping.
if len(match[1]) > 1 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Agree re:tests. I would like to see examples of such escaping format.

}
if fieldValue == nil {
return nil, errors.Errorf("field not found: %s", selector)
return nil, errors.Errorf("field not found: %s", reference.Path)
Copy link
Contributor

Choose a reason for hiding this comment

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

%q

{
Name: "res1aslice",
Resource: "res1",
Path: "a.slice[?(@.label==\"label2\")].value",
Copy link
Contributor

Choose a reason for hiding this comment

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

So we are not even referring to name inside the path or anywhere else, right? What's the purpose of name then?

obj := map[string]interface{}{
"ref": map[string]interface{}{
"slice": "{{res1#a.slice[?(@.label==\"label2\")].value}}",
"string": "{{res1#a.string}}",
"slice": "!{res1aslice}",
Copy link
Contributor

Choose a reason for hiding this comment

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

ok I see, we use name there.

@ash2k
Copy link
Contributor

ash2k commented Mar 26, 2018

@nilebox take a look at the proposal here #233 (comment) - this should clarify what is going on in this PR.

@ash2k
Copy link
Contributor

ash2k commented Mar 26, 2018

@wryun would be great to also update all the documentation to use the new syntax introduced in this PR.

@ash2k
Copy link
Contributor

ash2k commented Mar 26, 2018

@wryun To keep this PR smaller I suggest to remove escaping functionality from it. And then add it in a later PR. We need to discuss it first, I have a feeling it might require more code than we have here.

@ash2k ash2k force-pushed the refactor-dependson branch from c749934 to 1bc5334 Compare March 29, 2018 09:45
Copy link
Contributor

@ash2k ash2k left a comment

Choose a reason for hiding this comment

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

Some minor comments from me which I mostly addressed myself 😛

// Refer to a part of another object
type Reference struct {
Name ReferenceName `json:"name,omitempty"`
Resource ResourceName `json:"resource"`
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 probably be ResourceName ResourceName. But then it is a bit too verbose. WDYT?


resolvedRef, err := resolveReference(reference)
if err != nil {
finalerr = multierr.Append(finalerr, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this by design the there is no continue here to avoid adding invalid resolved ref into the map?

@ash2k
Copy link
Contributor

ash2k commented Mar 29, 2018

The remaining work here is to fix the docs.

@ash2k ash2k mentioned this pull request Apr 5, 2018
@ash2k
Copy link
Contributor

ash2k commented Apr 5, 2018

I'm going to merge this because I reeeally want to start tinkering with the "plugable controller" idea 😛
I've created #267 to track needed documentation work.

@ash2k ash2k merged commit 4cb5cd0 into master Apr 5, 2018
@ash2k ash2k deleted the refactor-dependson branch April 5, 2018 09:44
@wryun
Copy link
Contributor Author

wryun commented Apr 5, 2018

Ah, sorry. I had intended to get to it today, but house painting makes me lazy (actually, most things make me lazy).

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants