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

Send Application Sync request to Argo CD Server #44

Merged
merged 31 commits into from
Apr 5, 2021
Merged

Conversation

DiogoMCampos
Copy link
Contributor

@DiogoMCampos DiogoMCampos commented Mar 16, 2021

Issue link: #22

Opted to use the code behind the CLI, authenticating against the server based on secrets read from a local file. For the initial implementation, we opted to use Insecure.

What this does

  • Reads configuration from two files.
  • Creates a client to the argocd server.
  • Sends a sync request for every app that's scheduled to do so.
  • Sets up secrets to run controller locally as part of the set up scripts.

What this doesn't do

  • Doesn't handle certificates to connect securely to the argocd server;
  • Doesn't do anything on sync failure;

There's a couple of things missing that I intend to work on still, namely understanding what I need to change to the definitions/Makefile to use and mount the secrets as files.

@DiogoMCampos DiogoMCampos changed the title WIP: Application Sync Send Application Sync request to Argo CD Server Mar 19, 2021

if err != nil {
if strings.Contains(err.Error(), "another operation is already in progress") {
log.Info("another operation is already in progress", "app", s)
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 assume this means we proceed as normal - it's not an error per se - so I don't return here.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not really even sure we want to log this to be honest. This mean we tried to re sync the app while a sync was already in progress, correct? So what's the big deal? :)
I might miss a valid use case.

log.Info("another operation is already in progress", "app", s)
} else {
log.Error(err, "unable to sync app", "app", s)
return ctrl.Result{}, err
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this all we want to do? Do we want to somehow signal the stage as failed? Or is that future work?

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to somehow signal the stage as failed

For future work in #30 - as we don't do it at the moment

Copy link
Collaborator

Choose a reason for hiding this comment

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

What @maruina said... happy path only :-)

@@ -87,6 +87,13 @@ function local_argocd_login() {
echo "ARGOCD_LOCAL_ADDRESS=https://localhost:$argoserver_nodeport"
} >.env.local

if [ ! -d .prcconfig ]; then
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 sets up a config folder locally so we can make run and use it to connect to the argocd-server in kind.

r.Log.Info("syncing app", "app", s)

_, err := r.syncApp(s.Name)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The left-hand side variable is the synced *argov1alpha.Application, when there's no error. Is there anything we want to do with it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess it's to have a lambda style approach where you can do things like r.syncApps(s.Name).Status() or something like this in a single command.
Don't think we need to do something in particular right now.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We're actually doing something funny with that one. the sync from Argo takes an ApplicationRequest that we get from the Name. I guess the left hand side there is meant to tell you whether the selector worked. It could be useful down the line for us.

Copy link
Contributor

@maruina maruina left a comment

Choose a reason for hiding this comment

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

Just a couple of comment to answer some of your questions, I'll do a deeper review later.

controllers/progressiverollout_controller.go Outdated Show resolved Hide resolved

if err != nil {
if strings.Contains(err.Error(), "another operation is already in progress") {
log.Info("another operation is already in progress", "app", s)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not really even sure we want to log this to be honest. This mean we tried to re sync the app while a sync was already in progress, correct? So what's the big deal? :)
I might miss a valid use case.

log.Info("another operation is already in progress", "app", s)
} else {
log.Error(err, "unable to sync app", "app", s)
return ctrl.Result{}, err
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to somehow signal the stage as failed

For future work in #30 - as we don't do it at the moment

@@ -1,10 +1,10 @@

---
apiVersion: apiextensions.k8s.io/v1beta1
apiVersion: apiextensions.k8s.io/v1
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this the official release for v1?
Should we keep it a separate commit and release/package a specific version?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not at all. This was auto-generated by kubebuilder - I can revert the change and try to see where/why it was generated, but judging by https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/ v1beta1 seems to have been deprecated in v1.16?

Copy link
Collaborator

Choose a reason for hiding this comment

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

that confused the hell out of me :-)
my bad

Copy link
Contributor

Choose a reason for hiding this comment

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

If we use apiextensions.k8s.io/v1 it will not run in our current control cluster, so please let's skip this for a little bit.

ReadConfiguration returns a Configuration, reading it from environment variables if present, or from files in
the configuration directory otherwise
*/
func ReadConfiguration() (*Configuration, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

why using pointer semantics here?
we're using value semantics everywhere else?

Copy link
Collaborator

Choose a reason for hiding this comment

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

it's a minor thing, tbh, the logic of using pointer semantics makes sense so we can use nil. It's just against the decision made in #33.

@maruina what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Uhm - we can go with value semantics and return an empty config. As long as it also returns an error, we're able to handle it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Depending how we make things complicated, but it would be good to follow the same convention.

DiogoMCampos and others added 2 commits March 24, 2021 09:19
Co-authored-by: Sebastien Le Digabel <sledigabel@gmail.com>
README.md Outdated
make deploy
```

In order to so, a `.env.cluster` file needs to exist in the `config/manager` folder, containing
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure about adding a file into the kustomize directory. Is that the only way to make kustomize work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not very fond of it either, to be honest. But from https://kubectl.docs.kubernetes.io/faq/kustomize/: v2.0 added a security check that prevents kustomizations from reading files outside their own directory root.

This meant I can't find any file that's not inside that directory - I'm open to alternatives.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, so if I had not stopped reading at the first line, it says this:

Resources (including configmap and secret generators) can still be shared via the recommended best practice of placing them in a directory with their own kustomization file, and referring to this directory as a base from any kustomization that wants to use it. This encourages modularity and relocatability.

Let me investigate that.

README.md Outdated
2. Files in the Config Directory.
```
.
├── .prcconfig
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be ~/.prcconfig?

@@ -1,10 +1,10 @@

---
apiVersion: apiextensions.k8s.io/v1beta1
apiVersion: apiextensions.k8s.io/v1
Copy link
Contributor

Choose a reason for hiding this comment

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

If we use apiextensions.k8s.io/v1 it will not run in our current control cluster, so please let's skip this for a little bit.

@@ -36,4 +41,8 @@ spec:
requests:
cpu: 100m
memory: 20Mi
volumeMounts:
- mountPath: /.prcconfig
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure why we should use a .prcconfig in the root filesystem. Maybe /etc/prcconfig, or /opt/prcconfig or ~/.prcconfig?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, we should not. Which one do you prefer? I'm not unhappy with ~, but maybe /etc is a better place for it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

👍 for /etc/

go.mod Outdated
k8s.io/api v0.20.1
k8s.io/apimachinery v0.20.1
k8s.io/client-go v11.0.1-0.20190816222228-6d55c1b1f1ca+incompatible
k8s.io/kubernetes v1.20.1 // indirect
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you know where this is coming from?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tbh, no - it sure is weird because I've not installed nor updated any K8s-specific libraries. I'll get back to you on this.

ReadConfiguration returns a Configuration, reading it from environment variables if present, or from files in
the configuration directory otherwise
*/
func ReadConfiguration() (*Configuration, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Depending how we make things complicated, but it would be good to follow the same convention.

func GetArgoCDAppClient(c *Configuration) ArgoCDAppClient {
acdClientOpts := argocdclient.ClientOptions{
ServerAddr: c.ArgoCDServerAddr,
Insecure: true,
Copy link
Collaborator

Choose a reason for hiding this comment

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

mmmmh I'm not sure about this. could we parameterise this in the Configuration object instead of leaving this insecure by default?

@@ -22,6 +25,26 @@ const (
interval = time.Millisecond * 10
)

type mockArgoCDAppClientCounter struct {
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 there is something I don't understand here: why is this called Counter? Is that because we're counting how many apps we sync?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea of this mock is to mimic what in Java or Python would be assertCalledWith() or similar - I want to make sure that Sync() is called with a specific app name.

I think I called it Counter because initially I was just counting number of calls - but that's wrong, we want to know that a specific app was synced, rather than how many times apps where synced. So perhaps there's a better name for it?

Copy link
Contributor

@maruina maruina Mar 25, 2021

Choose a reason for hiding this comment

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

The idea of this mock is to mimic what in Java or Python would be assertCalledWith() or similar - I want to make sure that Sync() is called with a specific app name.

Thanks, now it make more sense to me. Maybe mockArgoCDAppClientWithApp?

"testing"
)

type mockArgoCDAppClientSyncOK struct{}
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 there is something that I'm missing here: why we don't have a mockArgoCDAppClient package with its own methods and tests?
The approach would be similar in https://github.com/Skyscanner/kms-issuer/tree/main/pkg/kmsmock

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For why the mocks are in the file and not a separate package, see the other comment - I'm happy to move them if we prefer that.

There's also the fact that I want different mocks to do different things - in fact, the one in suite_test is more like a stub than a mock per se, it's there just to make sure the other tests pass by default.

@@ -44,6 +47,12 @@ var k8sClient client.Client
var testEnv *envtest.Environment
var reconciler *ProgressiveRolloutReconciler

type mockArgoCDAppClient struct{}

func (*mockArgoCDAppClient) Sync(ctx context.Context, in *applicationpkg.ApplicationSyncRequest, opts ...grpc.CallOption) (*argov1alpha1.Application, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure I understand we we need to redefine the mockArgoCDAppClient and the sync method here. Can't we import it from a 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.

From what I saw, defining mocks in the test files they're used seems to be the most common practice (see https://github.com/kubernetes/kubernetes/blob/cea1d4e20b4a7886d8ff65f34c6d4f95efcb4742/staging/src/k8s.io/apiserver/pkg/authentication/token/union/unionauth_test.go#L30, for an example).

But if we prefer to have a package for the mocks, I'm happy to move it.

Copy link
Contributor

@maruina maruina Mar 25, 2021

Choose a reason for hiding this comment

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

I don't think I have good enough knowledge on the ecosystem and golang on this to be opinionated :)

I guess my point it was to be able to reuse the mock instead of redefining it multiple times? I guess when I see mockArgoCDAppClientCounter, mockArgoCDAppClient and mockArgoCDAppClientSyncOK they feel the same thing to me with different methods? But maybe not.

@sledigabel, @danmx, @Smirl: what do you think about this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They serve different purposes in different tests, but are all implementing the same interface:

  1. mockArgoCDAppClient is not a mock, it really is a generic stub so that we can run the other tests when we don't care about what happens in the calls to Argo CD.
  2. mockArgoCDAppClientCounter is really just a way to assertCalledWith the Sync() method - we could use this mock as the 'general' one (in 1.) if we so wished, I opted to make the other entirely useless to make it clear we don't care about its functionality.
  3. mockArgoCDAppClientSyncOK is used in unit testing to mock that the Argo CD app client returned OK, and conversely, SyncNotOK returns an error.

There's good things with this approach, imo: it's clear that each mock does something different, for instance. There's also bad things: if we ever opt to use one more method from the Argo CD App Client in the code, we have to update all these mocks, which adds toil to future maintainers.

Thoughts from those with more knowldege of Golang are most welcome here 🙂

Copy link

Choose a reason for hiding this comment

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

why bother creating your own mocks in the first place? Go can generate mocks for you (https://github.com/golang/mock) based on your interfaces.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Don't see any reason not to use it, but I've never used any way of mocking in Golang, so I'm not opinionated on it.

Taking a step back then: what is the approach we want to take w.r.t. mocking in this project? If it's using gomock, then let's change these mocks :). Again, I'm not opinionated either way, but would rather decide on one approach and have consistency going forward.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah i was going to suggest having a /mock package would be convenient, in case we need to reuse this for other tests moving forward.
Not sure about using mock to do that, but why not if that helps in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alright - so I did some playground stuff with gomock and I think that might be useful in the long term. Bearing in mind that the Sync is a blocker to other tasks, I've opted to simply move the mocks to a mocks package for the time being. We should still agree on how we're doing these things long-term.

@DiogoMCampos
Copy link
Contributor Author

DiogoMCampos commented Mar 26, 2021

Alright, I think I've addressed all comments - let me know if I haven't. A few notes:

  • I opted not to generate the secret with kustomize, as I feel Matteo is right on not wanting the .env file in those directories. Instead, I assume it already exists in the target cluster - for the purposes of development, the setup script generates said secret. If we are okay with this, I'll create a ticket for us to manage this secret in the future.
  • I think gomock is actually a very good shout - although, I did run into an issue with the Eventually/async tests and with gomock expecting a set number of calls in my experimentation. For the purposes of this PR, I'm suggesting we don't use it, but I think we should give it serious consideration going forward (and revisit the mocks in here).
  • As the reviews came in and the PR evolved, the configuration got a bit more complex than it was initially - at some point we should consider viper (or similar) instead of maintaining our own configuration manager.

@@ -0,0 +1,35 @@
package controllers
Copy link
Contributor

Choose a reason for hiding this comment

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

Question for the wider audience: do we want to have a separate test file for the unit tests, or can we merge those tests with progressiverollout_controller_test.go?

I would prefer a single test file for simplicity, but I'm open to suggestions

Copy link
Collaborator

Choose a reason for hiding this comment

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

There's an argument to separate integration tests vs unit tests as they look and are different.
How is it done on other operators?

Copy link
Contributor

Choose a reason for hiding this comment

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

I have never seen anything other controller.go and controller_test.go but I am happy if anyone can findaa different example.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In other languages (such as Python or Java), tests would be in their own folder, and I feel like I'd separate unit and other types of tests in different files too. So I felt this makes sense.

But Golang seems to be different in that tests generally live in the same package as the code under test; so perhaps a 1:1 relation between code-under-test and test-code files is ideal? If so, I can move them to the same file. I'm not opinionated on this.

internal/utils/config.go Outdated Show resolved Hide resolved
Co-authored-by: Matteo Ruina <matteo.ruina@gmail.com>
@maruina
Copy link
Contributor

maruina commented Mar 29, 2021

@DiogoMCampos I think we should also change the helm chart to exposes

ArgoCDAuthToken
ArgoCDServerAddr
ArgoCDInsecure

in the values.yaml and create the secret. It is then up to the end-user how to pass to token to the chart.

syncReq := applicationpkg.ApplicationSyncRequest{
Name: &appName,
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we have an opportunity here to validate that the app being synced is the app we passed as a parameter.
i'll raise a PR separately, it's not blocking that one.

Copy link
Contributor

@maruina maruina left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Collaborator

@sledigabel sledigabel left a comment

Choose a reason for hiding this comment

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

LGTM, adds a sync capability to the progressive rollout operator

@sledigabel
Copy link
Collaborator

Are we gonna merge this?

@DiogoMCampos DiogoMCampos merged commit 42c9374 into main Apr 5, 2021
@maruina maruina deleted the app-sync branch May 12, 2021 12:45
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.

None yet

4 participants