-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Better user experience with Kubernetes namespaces for UCP #1034
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
Better user experience with Kubernetes namespaces for UCP #1034
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The magic call to /kubernetesNamespaces is bothering me a bit, the fact that we hack the client out instead of extending/making the docker/docker/client composable, is bothering me way more..
cc @dnephin
cli/command/stack/list.go
Outdated
| "sort" | ||
|
|
||
| "k8s.io/apimachinery/pkg/api/errors" | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed, thanks.
cli/command/stack/list.go
Outdated
| } | ||
| opts.AllNamespaces = false | ||
| for _, nm := range nms.Items { | ||
| stacks, err = getStacks(stacks, dockerCli, opts, kubernetes.NewOptions(flags, nm.Name)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not reusing getStacksWithNamespaces here ?(instead of the loop)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getStacksWithNamespaces iterates on the namespaces given on the command line with --namespace, here it iterates on the namespaces retrieved by getUserVisibleNamespaces, so a different set of namespaces.
We could factorize the loops but we would need to add code to convert one of the data structures to the other one, and in the end we wouldn't save much…
cli/command/stack/list.go
Outdated
| TLSClientConfig: tlsConfig, | ||
| } | ||
| } | ||
| endpoint.Scheme = "https" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that we set https here, we should just error out if tlsOptions are nil above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, good point, thanks!
cli/command/stack/list.go
Outdated
| return mnms, nil | ||
| } | ||
|
|
||
| func getUserVisibleNamespaces(dockerCli command.Cli, forbiddenErr error) (*core_v1.NamespaceList, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really hacky… I feel that we should extract this somewhere in the kubernetes package and make and make the hack there (a func or list of func, …) … This would allow, in the future, to code different methods to get the list of namespace.
This also duplicates a bunch of what the cli/client does… We may want to Update https://github.com/moby/moby/blob/master/client/request.go to make it a bit more composable — like allowing to define new endpoint to query (with a function that would handle the http response).
cli/command/stack/list.go
Outdated
| func listUserNamespaces(httpClient *http.Client, endpoint url.URL) (*core_v1.NamespaceList, error) { | ||
| resp, err := httpClient.Get(endpoint.String()) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("unable to list user namespaces: %s", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here (and later on), prefer to use github.com/pkg/errors with Wrap or Wrapf :
return nil, errors.Wrap(err, "unable to list user namespaces")There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noted, thanks!
dnephin
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
generally LGTM, one comment
| flags.StringVar(&opts.Format, "format", "", "Pretty-print stacks using a Go template") | ||
| flags.StringSliceVar(&opts.Namespaces, "namespace", []string{"default"}, "Kubernetes namespaces to use") | ||
| flags.SetAnnotation("namespace", "kubernetes", nil) | ||
| flags.SetAnnotation("namespace", "experimentalCLI", nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't this use InstallFlags() (which I would suggest should be renamed to AddNamespaceFlag().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can't because here it's
flags.StringSliceVar(&opts.Namespaces, "namespace", []string{"default"}, "Kubernetes namespaces to use")
whereas in InstallFlags soon to be AddNamespaceFlag it's
flags.String("namespace", "default", "Kubernetes namespace to use")
e.g. most docker stack sub-commands accept only one --namespace but docker stack ls can handle a list of them.
cli/command/stack/kubernetes/cli.go
Outdated
| } | ||
|
|
||
| // InstallFlags adds common Kubernetes related flags | ||
| func InstallFlags(flags *flag.FlagSet) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, 2 comments :)
s/InstallFlags/AddNamespaceFlag/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, thanks!
|
@vdemeester I added a commit to move the code into the kubernetes package and started to refactor it for toward using moby client directly, for which I have a prototype in https://github.com/mat007/cli/commit/fee52e12c225a4c9a04d480ef75ec06675b169bf |
| return nil, err | ||
| } | ||
| endpoint.Scheme = "https" | ||
| endpoint.Path = "/kubernetesNamespaces" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UCP does not have a versioned API at present. If we're going to rely on UCP endpoints, we need to be careful about making sure that this stays backwards-compatible.
Is this UX a requirement? Kubernetes does not have any other operations where you can view a list of only the resources that you have access to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, this is really fragile and ad hoc, however this is indeed a UX requirement and Kubernetes does not provide this feature…
If you have any suggestion on how to better manage this I'll be happy to update the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UCP does not have a versioned API at present. If we're going to rely on UCP endpoints, we need to be careful about making sure that this stays backwards-compatible.
@wsong yes, that's the job of UCP to be backwards-compatible on that endpoint (or providing a versionned one)…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All right, let's stick with this approach for now, but we'll have to be careful to test this with different combinations of CLI version + UCP version.
cli/command/container/create_test.go
Outdated
| return container.ContainerCreateCreatedBody{}, fmt.Errorf("shouldn't try to pull image") | ||
| }, | ||
| }, test.EnableContentTrust) | ||
| }).EnableContentTrust() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this change ? I tend to prefer functionnal argument that "chains" of call… If it's just to not have test., we can dot import…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe @thaJeztah suggested it, I personally don't have any strong opinion on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the way this commit actually belongs to the PR before this one e.g. #1031
cli/command/stack/list.go
Outdated
| return ss, nil | ||
| } | ||
|
|
||
| func removeDuplicates(namespaces []string) []string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good point.
|
@vdemeester I tweaked the custom request a bit and opened a PR upstream, can you please take another look? |
| return nil, err | ||
| } | ||
| endpoint.Scheme = "https" | ||
| endpoint.Path = "/kubernetesNamespaces" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All right, let's stick with this approach for now, but we'll have to be careful to test this with different combinations of CLI version + UCP version.
wsong
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops, didn't mean to approve.
cli/command/stack/kubernetes/list.go
Outdated
| if err != nil { | ||
| return nil, errors.Wrapf(err, "received %d status and unable to read response", resp.StatusCode) | ||
| } | ||
| if resp.StatusCode != http.StatusOK { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're not talking to a UCP cluster, this is going to return a 404, and you'll get back a very ugly error message that just says "page not found" or something. You should probably have some sort of fallback behavior here if /kubernetesNamespaces returns a 404.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getUserVisibleNamespaces returns an error indeed, the fallback is implemented by the caller in https://github.com/docker/cli/pull/1034/files#diff-5199fcbf9bc932c71a9e1984b1952b7aR62
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but you just log a debug message there and return err.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It logs err2, the error returned by getUserVisibleNamespaces, and then returns err from the previous call, which is different and in practice due to the apierrs.IsForbidden(err) test right before is effectively as if getUserVisibleNamespaces had not been called at all:
$ docker --orchestrator=all stack ls
stacks.compose.docker.com is forbidden: User "user" cannot list stacks.compose.docker.com at the cluster scope: access denied
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. I think a comment here would be helpful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I added
// ignore this last error and return the previous one instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, thinking:
- A 404 error is not an error, just means: we don't have UCP
- We're now hiding potential other errors (500 - UCP crashed, 403 - UCP is up, but credentials are incorrect (?), ??? - Other error)
Instead of returning expected errors, and ignoring them in getStacksWithAllNamespaces(), we should leave the logic/decision if it's an actual error or not to getUserVisibleNamespaces(). When running into a 404, just return an empty list.
func getStacksWithAllNamespaces(dockerCli command.Cli, kubeCli *KubeCli, opts options.List) ([]*formatter.Stack, error) {
stacks, err := getStacks(kubeCli, opts)
if err == nil || !apierrs.IsForbidden(err) {
return stacks, err
}
nms, err2 := getUserVisibleNamespaces(dockerCli)
if err2 != nil {
return nil, errors.Wrap(err2, "Failed to query user visible namespaces")
}
if err != nil {
return nil, errors.Wrap(err, "Failed to retrieve stacks")
}
opts.AllNamespaces = false
for _, namespace := range nms.Items {
kubeCli.kubeNamespace = namespace.Name
ss, err := getStacks(kubeCli, opts)
if err != nil {
return nil, err
}
stacks = append(stacks, ss...)
}
return stacks, nil
}
func getUserVisibleNamespaces(dockerCli command.Cli) (*core_v1.NamespaceList, error) {
host := dockerCli.Client().DaemonHost()
endpoint, err := url.Parse(host)
if err != nil {
return nil, err
}
endpoint.Scheme = "https"
endpoint.Path = "/kubernetesNamespaces"
resp, err := dockerCli.Client().HTTPClient().Get(endpoint.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrapf(err, "received %d status and unable to read response", resp.StatusCode)
}
namespaces := &core_v1.NamespaceList{}
switch resp.StatusCode {
case http.StatusOK:
if err := json.Unmarshal(body, namespaces); err != nil {
return nil, errors.Wrapf(err, "unmarshal failed: %s", string(body))
}
case http.StatusNotFound:
// UCP API not present
default:
return nil, fmt.Errorf("received %d status while retrieving namespaces: %s", resp.StatusCode, string(body))
}
return namespaces, nil
}diff --git a/cli/command/stack/kubernetes/list.go b/cli/command/stack/kubernetes/list.go
index 8a7b1406..cb467f9b 100644
--- a/cli/command/stack/kubernetes/list.go
+++ b/cli/command/stack/kubernetes/list.go
@@ -11,7 +11,6 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/stack/options"
"github.com/pkg/errors"
- "github.com/sirupsen/logrus"
core_v1 "k8s.io/api/core/v1"
apierrs "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -61,9 +60,10 @@ func getStacksWithAllNamespaces(dockerCli command.Cli, kubeCli *KubeCli, opts op
}
nms, err2 := getUserVisibleNamespaces(dockerCli)
if err2 != nil {
- logrus.Debugf("Failed to query user visible namespaces: %s", err2)
- // ignore this last error and return the previous one instead
- return nil, err
+ return nil, errors.Wrap(err2, "Failed to query user visible namespaces")
+ }
+ if err != nil {
+ return nil, errors.Wrap(err, "Failed to retrieve stacks")
}
opts.AllNamespaces = false
for _, namespace := range nms.Items {
@@ -94,12 +94,16 @@ func getUserVisibleNamespaces(dockerCli command.Cli) (*core_v1.NamespaceList, er
if err != nil {
return nil, errors.Wrapf(err, "received %d status and unable to read response", resp.StatusCode)
}
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf(string(body))
- }
namespaces := &core_v1.NamespaceList{}
- if err := json.Unmarshal(body, namespaces); err != nil {
- return nil, errors.Wrapf(err, "unmarshal failed: %s", string(body))
+ switch resp.StatusCode {
+ case http.StatusOK:
+ if err := json.Unmarshal(body, namespaces); err != nil {
+ return nil, errors.Wrapf(err, "unmarshal failed: %s", string(body))
+ }
+ case http.StatusNotFound:
+ // UCP API not present
+ default:
+ return nil, fmt.Errorf("received %d status while retrieving namespaces: %s", resp.StatusCode, string(body))
}
return namespaces, nil
}| if err != nil { | ||
| return nil, err | ||
| } | ||
| endpoint.Scheme = "https" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part is a bit sad as this is something we already do in the moby/moby client package (setting the scheme to https when tls is configured)..
cli/command/stack/kubernetes/list.go
Outdated
| if err != nil { | ||
| return nil, errors.Wrapf(err, "received %d status and unable to read response", resp.StatusCode) | ||
| } | ||
| if resp.StatusCode != http.StatusOK { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, thinking:
- A 404 error is not an error, just means: we don't have UCP
- We're now hiding potential other errors (500 - UCP crashed, 403 - UCP is up, but credentials are incorrect (?), ??? - Other error)
Instead of returning expected errors, and ignoring them in getStacksWithAllNamespaces(), we should leave the logic/decision if it's an actual error or not to getUserVisibleNamespaces(). When running into a 404, just return an empty list.
func getStacksWithAllNamespaces(dockerCli command.Cli, kubeCli *KubeCli, opts options.List) ([]*formatter.Stack, error) {
stacks, err := getStacks(kubeCli, opts)
if err == nil || !apierrs.IsForbidden(err) {
return stacks, err
}
nms, err2 := getUserVisibleNamespaces(dockerCli)
if err2 != nil {
return nil, errors.Wrap(err2, "Failed to query user visible namespaces")
}
if err != nil {
return nil, errors.Wrap(err, "Failed to retrieve stacks")
}
opts.AllNamespaces = false
for _, namespace := range nms.Items {
kubeCli.kubeNamespace = namespace.Name
ss, err := getStacks(kubeCli, opts)
if err != nil {
return nil, err
}
stacks = append(stacks, ss...)
}
return stacks, nil
}
func getUserVisibleNamespaces(dockerCli command.Cli) (*core_v1.NamespaceList, error) {
host := dockerCli.Client().DaemonHost()
endpoint, err := url.Parse(host)
if err != nil {
return nil, err
}
endpoint.Scheme = "https"
endpoint.Path = "/kubernetesNamespaces"
resp, err := dockerCli.Client().HTTPClient().Get(endpoint.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrapf(err, "received %d status and unable to read response", resp.StatusCode)
}
namespaces := &core_v1.NamespaceList{}
switch resp.StatusCode {
case http.StatusOK:
if err := json.Unmarshal(body, namespaces); err != nil {
return nil, errors.Wrapf(err, "unmarshal failed: %s", string(body))
}
case http.StatusNotFound:
// UCP API not present
default:
return nil, fmt.Errorf("received %d status while retrieving namespaces: %s", resp.StatusCode, string(body))
}
return namespaces, nil
}diff --git a/cli/command/stack/kubernetes/list.go b/cli/command/stack/kubernetes/list.go
index 8a7b1406..cb467f9b 100644
--- a/cli/command/stack/kubernetes/list.go
+++ b/cli/command/stack/kubernetes/list.go
@@ -11,7 +11,6 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/stack/options"
"github.com/pkg/errors"
- "github.com/sirupsen/logrus"
core_v1 "k8s.io/api/core/v1"
apierrs "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -61,9 +60,10 @@ func getStacksWithAllNamespaces(dockerCli command.Cli, kubeCli *KubeCli, opts op
}
nms, err2 := getUserVisibleNamespaces(dockerCli)
if err2 != nil {
- logrus.Debugf("Failed to query user visible namespaces: %s", err2)
- // ignore this last error and return the previous one instead
- return nil, err
+ return nil, errors.Wrap(err2, "Failed to query user visible namespaces")
+ }
+ if err != nil {
+ return nil, errors.Wrap(err, "Failed to retrieve stacks")
}
opts.AllNamespaces = false
for _, namespace := range nms.Items {
@@ -94,12 +94,16 @@ func getUserVisibleNamespaces(dockerCli command.Cli) (*core_v1.NamespaceList, er
if err != nil {
return nil, errors.Wrapf(err, "received %d status and unable to read response", resp.StatusCode)
}
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf(string(body))
- }
namespaces := &core_v1.NamespaceList{}
- if err := json.Unmarshal(body, namespaces); err != nil {
- return nil, errors.Wrapf(err, "unmarshal failed: %s", string(body))
+ switch resp.StatusCode {
+ case http.StatusOK:
+ if err := json.Unmarshal(body, namespaces); err != nil {
+ return nil, errors.Wrapf(err, "unmarshal failed: %s", string(body))
+ }
+ case http.StatusNotFound:
+ // UCP API not present
+ default:
+ return nil, fmt.Errorf("received %d status while retrieving namespaces: %s", resp.StatusCode, string(body))
}
return namespaces, nil
}| } | ||
| endpoint.Scheme = "https" | ||
| endpoint.Path = "/kubernetesNamespaces" | ||
| resp, err := dockerCli.Client().HTTPClient().Get(endpoint.String()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering if there are other things that should be set on the request (content-type JSON, custom headers that are configured in the client-configuration, etc);
cli/vendor/github.com/docker/docker/client/request.go
Lines 225 to 241 in 60930d3
| func (cli *Client) addHeaders(req *http.Request, headers headers) *http.Request { | |
| // Add CLI Config's HTTP Headers BEFORE we set the Docker headers | |
| // then the user can't change OUR headers | |
| for k, v := range cli.customHTTPHeaders { | |
| if versions.LessThan(cli.version, "1.25") && k == "User-Agent" { | |
| continue | |
| } | |
| req.Header.Set(k, v) | |
| } | |
| if headers != nil { | |
| for k, v := range headers { | |
| req.Header[k] = v | |
| } | |
| } | |
| return req | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know, however this would not be an issue were the Client performing the request itself.
moby/moby#37071 has been merged in moby, but maybe it's not to late to bring that up?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, it's a bit of an odd situation; the /kubernetesNamespaces endpoint feels like an add-hoc/temporary solution. Some thoughts on that;
- It's undocumented, and not sure how well-defined the endpoint is
- the Engine remote API may perform validations, e.g.
- correct request-type, respond different based on "accept" headers (plain-text/JSON)
- version negotiation (not present in the UCP API) (this could be a problem if we used the
docker/client/requestfunctionality, as it may be done automatically - need to check)
So, should we export the docker/client/request client.get(), client.head() and so on functions? I don't know.
On the "plus" side; when using the docker/client/request functionality, we would get some things for free (improved/consistent errors if TLS fails, automatic handling of invalid JSON responses, etc.)
I think in an ideal situation, given that UCP defines its own API (which is the Engine remote-API + Additional endpoints/extensions) there would be an official UCP client that we could use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do agree with @thaJeztah that it is really an odd situation : we end up hacking the docker/docker client around for something that isn't even docker api related.
As discussed a bit offline earlier, a cleaner way (way cleaner) would have been to have a kubernetes api extension (same as the current "compose on kubernetes" one) :
- we wouldn't need to hack
docker/dockerclient around, we would just consume the go code from the extension (again, same way as "compose on kubernetes") - it would be "automatically" versionned (because of k8s way of doing things)
- it wouldn't be a direct dependency on UCP as it could be easily deployed on other cluster in the future if we wanted
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Being new to the org I cannot estimate whether this is a realistic solution given the time constraint, @chris-crone any opinion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@thaJeztah there's an issue against the relevant repo to ensure that this endpoint is properly tested and that its behaviour isn't modified.
@vdemeester I agree that those solutions would be cleaner but given the timeline for this; they're not feasible. Since this is a fallback we can replace it or demote it in the fallback list at a later date once we've had time to investigate the cleaner options.
|
I updated |
|
Obviously |
cli/command/stack/kubernetes/list.go
Outdated
| return stacks, nil | ||
| } | ||
|
|
||
| func getUserVisibleNamespaces(dockerCli command.Cli, previousErr error) (*core_v1.NamespaceList, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@thaJeztah @mat007 I think it's a little weird to pass in previousErr to this function so that we can return it in certain cases.
@thaJeztah, what did you have in mind here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose I could return also a bool meaning «404, just ignore the call»? Or create a custom error to be returned just for the sake a filtering right after the call?
The previousErr remains the solution with the less extra code, which is why I went for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think your previous solution (return an error and then have getStacksWithAllNamespaces check for that err and return the previous error) was actually better, which is why I'm wondering if @thaJeztah had something different in mind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My suggestion (here #1034 (comment)) was because the previous solution was discarding all errors returned by UCP, which was definitely not the right thing to do.
I see I made one mistake in that example, which probably lead to @mat007 rewriting it (updated version below);
updated example:
func getStacksWithAllNamespaces(dockerCli command.Cli, kubeCli *KubeCli, opts options.List) ([]*formatter.Stack, error) {
stacks, err := getStacks(kubeCli, opts)
if err == nil || !apierrs.IsForbidden(err) {
return stacks, err
}
nms, err2 := getUserVisibleNamespaces(dockerCli)
if err2 != nil {
return nil, errors.Wrap(err2, "Failed to query user visible namespaces")
}
if nms == nil {
return nil, errors.Wrap(err, "Failed to retrieve stacks")
}
opts.AllNamespaces = false
for _, namespace := range nms.Items {
kubeCli.kubeNamespace = namespace.Name
ss, err := getStacks(kubeCli, opts)
if err != nil {
return nil, err
}
stacks = append(stacks, ss...)
}
return stacks, nil
}
func getUserVisibleNamespaces(dockerCli command.Cli) (*core_v1.NamespaceList, error) {
host := dockerCli.Client().DaemonHost()
endpoint, err := url.Parse(host)
if err != nil {
return nil, err
}
endpoint.Scheme = "https"
endpoint.Path = "/kubernetesNamespaces"
resp, err := dockerCli.Client().HTTPClient().Get(endpoint.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrapf(err, "received %d status and unable to read response", resp.StatusCode)
}
switch resp.StatusCode {
case http.StatusOK:
namespaces := &core_v1.NamespaceList{}
if err := json.Unmarshal(body, namespaces); err != nil {
return nil, errors.Wrapf(err, "unmarshal failed: %s", string(body))
}
return namespaces, nil
case http.StatusNotFound:
// UCP API not present
return nil, nil
default:
return nil, fmt.Errorf("received %d status while retrieving namespaces: %s", resp.StatusCode, string(body))
}
}if stacks, err := getUCPStacks(dockerCli, opts); err != nil || stacks != nil {
return stacks, err
}
return getStacks(kubeCli, opts)In the above;
-
In case of a 404,
getUCPStacks()returns no error, andnilfor stacks, in that case, fall back to plain kubernetes -
Any other error is an issue with the UCP endpoint, so should be handled. In that case, don't fallback to plain kubernetes
-
Advantage: There's no back and forth between trying UCP and k8s; currently, we try k8s, get an error, try UCP, discover it's not there, and head back to the original k8s error
-
Disadvantage: It's odd for Docker CE clients to try an API that won't be there for 99% of the users
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we cannot swap the function calls as what we want is first Kubernetes, then UCP only if Kubernetes fails for lack of user rights.
@wsong we have 3 possible results when querying UCP : not there (404), there but error'ed and valid result, so we need to distinguish between the three
@thaJeztah ah yes, I suppose we could test for nil to distinguish a 404 from []string{} an empty result, OK I'll do that instead
Thank you both!
cli/command/stack/kubernetes/list.go
Outdated
| } | ||
| func getStacksWithAllNamespaces(dockerCli command.Cli, kubeCli *KubeCli, opts options.List) ([]*formatter.Stack, error) { | ||
| stacks, err := getStacks(kubeCli, opts) | ||
| if err == nil || !apierrs.IsForbidden(err) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this could just be;
if !apierrs.IsForbidden(err) {
return stacks, err
}(no need to change)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, I'll change it while I'm at it.
cli/command/stack/kubernetes/list.go
Outdated
| return nil, errors.Wrap(err, "failed to query user visible namespaces") | ||
| } | ||
| opts.AllNamespaces = false | ||
| for _, namespace := range nms.Items { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May not be worth changing in this PR, but just realised if we change getUserVisibleNamespaces() to return a []string, we could use getStacksWithNamespaces() here
cli/command/stack/kubernetes/list.go
Outdated
| } | ||
| return formattedStacks, nil | ||
| } | ||
| func getStacksWithAllNamespaces(dockerCli command.Cli, kubeCli *KubeCli, opts options.List) ([]*formatter.Stack, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heh, also just occurred to me: KubeCli is also a command.Cli (see WrapCLI()); so you'd only need to pass in kubeCli
cli/command/stack/kubernetes/list.go
Outdated
| return stacks, nil | ||
| } | ||
|
|
||
| func getUserVisibleNamespaces(dockerCli command.Cli, previousErr error) (*core_v1.NamespaceList, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My suggestion (here #1034 (comment)) was because the previous solution was discarding all errors returned by UCP, which was definitely not the right thing to do.
I see I made one mistake in that example, which probably lead to @mat007 rewriting it (updated version below);
updated example:
func getStacksWithAllNamespaces(dockerCli command.Cli, kubeCli *KubeCli, opts options.List) ([]*formatter.Stack, error) {
stacks, err := getStacks(kubeCli, opts)
if err == nil || !apierrs.IsForbidden(err) {
return stacks, err
}
nms, err2 := getUserVisibleNamespaces(dockerCli)
if err2 != nil {
return nil, errors.Wrap(err2, "Failed to query user visible namespaces")
}
if nms == nil {
return nil, errors.Wrap(err, "Failed to retrieve stacks")
}
opts.AllNamespaces = false
for _, namespace := range nms.Items {
kubeCli.kubeNamespace = namespace.Name
ss, err := getStacks(kubeCli, opts)
if err != nil {
return nil, err
}
stacks = append(stacks, ss...)
}
return stacks, nil
}
func getUserVisibleNamespaces(dockerCli command.Cli) (*core_v1.NamespaceList, error) {
host := dockerCli.Client().DaemonHost()
endpoint, err := url.Parse(host)
if err != nil {
return nil, err
}
endpoint.Scheme = "https"
endpoint.Path = "/kubernetesNamespaces"
resp, err := dockerCli.Client().HTTPClient().Get(endpoint.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrapf(err, "received %d status and unable to read response", resp.StatusCode)
}
switch resp.StatusCode {
case http.StatusOK:
namespaces := &core_v1.NamespaceList{}
if err := json.Unmarshal(body, namespaces); err != nil {
return nil, errors.Wrapf(err, "unmarshal failed: %s", string(body))
}
return namespaces, nil
case http.StatusNotFound:
// UCP API not present
return nil, nil
default:
return nil, fmt.Errorf("received %d status while retrieving namespaces: %s", resp.StatusCode, string(body))
}
}if stacks, err := getUCPStacks(dockerCli, opts); err != nil || stacks != nil {
return stacks, err
}
return getStacks(kubeCli, opts)In the above;
-
In case of a 404,
getUCPStacks()returns no error, andnilfor stacks, in that case, fall back to plain kubernetes -
Any other error is an issue with the UCP endpoint, so should be handled. In that case, don't fallback to plain kubernetes
-
Advantage: There's no back and forth between trying UCP and k8s; currently, we try k8s, get an error, try UCP, discover it's not there, and head back to the original k8s error
-
Disadvantage: It's odd for Docker CE clients to try an API that won't be there for 99% of the users
|
@thaJeztah PR updated! |
thaJeztah
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changes LGTM
(but we should definitely look into the UCP implementation for a future update)
I saw that the vendor-check failed, so may have to check that
Also bringing: . golang.org/x/net 5561cd9b4330353950f399814f427425c0a26fd2 . github.com/docker/distribution 83389a148052d74ac602f5f1d62f86ff2f3c4aa5 . github.com/docker/swarmkit bd69f6e8e301645afd344913fa1ede53a0a111fb . github.com/docker/go-metrics d466d4f6fd960e01820085bd7e1a24426ee7ef18 . github.com/prometheus/client_golang 52437c81da6b127a9925d17eb3a382a2e5fd395e . github.com/beorn7/perks 4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9 . github.com/prometheus/client_model fa8ad6fec33561be4280a8f0514318c79d7f6cb6 . github.com/prometheus/common ebdfc6da46522d58825777cf1f90490a5b1ef1d8 . github.com/prometheus/procfs abf152e5f3e97f2fafac028d2cc06c1feb87ffa5 . github.com/matttproud/golang_protobuf_extensions v1.0.0 Signed-off-by: Mathieu Champlon <mathieu.champlon@docker.com>
Signed-off-by: Mathieu Champlon <mathieu.champlon@docker.com>
vdemeester
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SGTM 🍂
|
Merged! @chris-crone can you handle opening tickets for the follow-up (better implementation of the all namespaces endpoint)? |
- What I did
Made docker stack ls with the Kubernetes orchestrator list the namespaces a non-administrator user
has access to.
- How I did it
As this use case cannot be handled using the native Kubernetes API, I had to use an ad hoc workaround in the form of a call to the UCP API as a backup in the event of the call to the Kubernetes API failing.
Also as there is no simple means to retrieve the http client from the docker client, a new http client is being created…
- How to verify it
Create a Kubernetes user 'my-user' and grant it only access to the namespace of some installed services.
Without this PR docker stack ls fails when
whereas with the PR
- Description for the changelog
None as this is merely a tweak for what was added in #1031
- A picture of a cute animal (not mandatory but encouraged)