diff --git a/cmd/kubehook/kubehook.go b/cmd/kubehook/kubehook.go index 3df13f4..428c758 100644 --- a/cmd/kubehook/kubehook.go +++ b/cmd/kubehook/kubehook.go @@ -32,6 +32,7 @@ import ( "github.com/planetlabs/kubehook/auth/jwt" "github.com/planetlabs/kubehook/handlers" "github.com/planetlabs/kubehook/handlers/authenticate" + "github.com/planetlabs/kubehook/handlers/client" "github.com/planetlabs/kubehook/handlers/generate" "github.com/planetlabs/kubehook/handlers/kubecfg" _ "github.com/planetlabs/kubehook/statik" @@ -193,8 +194,10 @@ func main() { t, err := kubecfg.LoadTemplate(*template) kingpin.FatalIfError(err, "cannot load kubeconfig template") r.HandlerFunc("GET", "/kubecfg", kubecfg.Handler(m, t, h)) + r.HandlerFunc("GET", "/client", client.Handler(*maxlife, t)) } else { r.HandlerFunc("GET", "/kubecfg", handlers.NotImplemented()) + r.HandlerFunc("GET", "/client", client.Handler(*maxlife, nil)) } log.Info("shutdown", zap.Error(listenAndServe(s, *tlsCert, *tlsKey))) diff --git a/frontend/package.json b/frontend/package.json index 608f29e..214d0ba 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,7 +19,7 @@ "vue-axios": "^2.0.2", "vue-highlightjs": "^1.3.3", "vue-meta": "^1.4.0", - "vue-slider-component": "^2.4.7" + "vue-slider-component": "^2.8.4" }, "browserslist": [ "> 1%", diff --git a/frontend/src/kubehook.vue b/frontend/src/kubehook.vue index b6f3f64..85f3821 100644 --- a/frontend/src/kubehook.vue +++ b/frontend/src/kubehook.vue @@ -59,10 +59,7 @@ Token lifetime
@@ -88,6 +85,12 @@ export default { }, data: function() { return { + slider: { + min: 1, + max: 7, + tooltipDir: "bottom", + formatter: "{value} days" + }, kubecfg: false, lifetime: 2, clusterID: "radcluster", @@ -95,6 +98,9 @@ export default { error: null }; }, + mounted: function() { + this.fetchConfig(); + }, created: function() { this.detectKubeCfg(); }, @@ -102,6 +108,25 @@ export default { inHours: function(lifetime) { return lifetime * 24 + "h"; }, + inDays: function(lifetime) { + return Math.floor(lifetime / 24); + }, + fetchConfig: function() { + var _this = this; + this.axios + .get("/client") + .then(function(response) { + _this.slider.max = _this.inDays(response.data.max_lifetime); + _this.clusterID = response.data.cluster_id; + }) + .catch(function(e) { + if (e.request) { + _this.error = "could not connect to API"; + return; + } + _this.error = e; + }); + }, detectKubeCfg: function() { var _this = this; this.axios diff --git a/handlers/client/client.go b/handlers/client/client.go new file mode 100644 index 0000000..a673e90 --- /dev/null +++ b/handlers/client/client.go @@ -0,0 +1,62 @@ +/* +Copyright 2018 Planet Labs Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing permissions +and limitations under the License. +*/ + +package client + +import ( + "encoding/json" + "net/http" + "time" + + "k8s.io/client-go/tools/clientcmd/api" +) + + +const ( + defaultClusterId = "radcluster" +) + +type rsp struct { + ClusterID string `json:"cluster_id,omitempty"` + MaxLifetime float64 `json:"max_lifetime,omitempty"` +} + +// Handler returns an HTTP handler function that provides the client config. +func Handler(lifetime time.Duration, template *api.Config) http.HandlerFunc { + data := rsp { + ClusterID: defaultClusterId, + MaxLifetime: lifetime.Hours(), + } + + if template != nil { + for name, _ := range template.Clusters { + data.ClusterID = name + break + } + } + + return func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + write(w, data, http.StatusOK) + } +} + +func write(w http.ResponseWriter, data interface{}, httpStatus int) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(httpStatus) + json.NewEncoder(w).Encode(data) // nolint: gosec +} diff --git a/handlers/client/client_test.go b/handlers/client/client_test.go new file mode 100644 index 0000000..b16310e --- /dev/null +++ b/handlers/client/client_test.go @@ -0,0 +1,88 @@ +/* +Copyright 2018 Planet Labs Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing permissions +and limitations under the License. +*/ + +package client + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/go-test/deep" + "k8s.io/client-go/tools/clientcmd/api" +) + + +const ( + testCluster = "test" +) + +var ( + testDuration, _ = time.ParseDuration("10h") + testTemplate = &api.Config { + Clusters: map[string]*api.Cluster { + testCluster: &api.Cluster { + }, + }, + } +) + +func TestHandler(t *testing.T) { + cases := map[string]struct { + lifetime time.Duration + template *api.Config + rsp *rsp + }{ + "Default Cluster": { + lifetime: testDuration, + rsp: &rsp{ + MaxLifetime: testDuration.Hours(), + ClusterID: defaultClusterId, + }, + }, + "Template Cluster": { + lifetime: testDuration, + template: testTemplate, + rsp: &rsp{ + MaxLifetime: testDuration.Hours(), + ClusterID: testCluster, + }, + }, + } + for testName, tt := range cases { + t.Run(testName, func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + Handler(tt.lifetime, tt.template)(w, r) + + expectedStatus := http.StatusOK + if w.Code != expectedStatus { + t.Errorf("w.Code: want %v, got %v", expectedStatus, w.Code) + } + + rsp := &rsp{} + if err := json.Unmarshal(w.Body.Bytes(), rsp); err != nil { + t.Fatalf("json.Unmarshal(%v, %s): %v", w.Body, rsp, err) + } + + if diff := deep.Equal(tt.rsp, rsp); diff != nil { + t.Errorf("want != got: %v", diff) + } + }) + } +}