diff --git a/pkg/agent/entry.go b/pkg/agent/entry.go index 7b0bb28d..d0e5b5ca 100644 --- a/pkg/agent/entry.go +++ b/pkg/agent/entry.go @@ -22,6 +22,7 @@ import ( "github.com/urfave/cli" "golang.org/x/net/netutil" "google.golang.org/grpc" + authentication "k8s.io/api/authentication/v1" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -101,8 +102,10 @@ func (a *agentConfig) String() string { type agent struct { cfg *agentConfig + userInfo authentication.UserInfo listener net.Listener namespaces kube.Namespaces + tokens kube.Tokens remoteAPI promapiv1.API } @@ -181,10 +184,19 @@ func createAgent(cfg *agentConfig) (*agent, error) { return nil, errors.Annotate(err, "unable to new Prometheus client") } + // create tokens client and get userInfo + tokens := kube.NewTokens(cfg.ctx, k8sClient) + userInfo, err := tokens.Authenticate(cfg.myToken) + if err != nil { + return nil, errors.Annotate(err, "unable to get userInfo from agent token") + } + return &agent{ cfg: cfg, + userInfo: userInfo, listener: listener, namespaces: kube.NewNamespaces(cfg.ctx, k8sClient), + tokens: tokens, remoteAPI: promapiv1.NewAPI(promClient), }, nil } diff --git a/pkg/agent/http.go b/pkg/agent/http.go index 3e46a783..e174f1f6 100644 --- a/pkg/agent/http.go +++ b/pkg/agent/http.go @@ -9,8 +9,11 @@ import ( "time" "github.com/gorilla/mux" + "github.com/juju/errors" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/rancher/prometheus-auth/pkg/kube" log "github.com/sirupsen/logrus" + authentication "k8s.io/api/authentication/v1" ) func (a *agent) httpBackend() http.Handler { @@ -59,14 +62,25 @@ func accessControl(agt *agent, proxyHandler http.Handler) http.Handler { router.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var userInfo authentication.UserInfo + var err error accessToken := strings.TrimPrefix(r.Header.Get(authorizationHeaderKey), "Bearer ") + + // try to authenticate the access token if len(accessToken) == 0 { - http.Error(w, "unauthorized", http.StatusUnauthorized) + err = errors.New("no access token provided") + } else { + userInfo, err = agt.tokens.Authenticate(accessToken) + } + + if err != nil { + // either not token was provided or user is unauthenticated with k8s API + http.Error(w, err.Error(), http.StatusUnauthorized) return } // direct proxy - if agt.cfg.myToken == accessToken { + if kube.MatchingUsers(agt.userInfo, userInfo) { proxyHandler.ServeHTTP(w, r) return } diff --git a/pkg/agent/http_api_context.go b/pkg/agent/http_api_context.go index a6bc6dc2..68d73c16 100644 --- a/pkg/agent/http_api_context.go +++ b/pkg/agent/http_api_context.go @@ -7,7 +7,7 @@ import ( "sync" "github.com/cockroachdb/cockroach/pkg/util/httputil" - "github.com/golang/protobuf/proto" + "github.com/gogo/protobuf/proto" "github.com/golang/snappy" "github.com/juju/errors" promapiv1 "github.com/prometheus/client_golang/api/prometheus/v1" diff --git a/pkg/agent/http_test.go b/pkg/agent/http_test.go index 6de68547..e224993d 100644 --- a/pkg/agent/http_test.go +++ b/pkg/agent/http_test.go @@ -36,6 +36,7 @@ import ( "github.com/rancher/prometheus-auth/pkg/data" "github.com/rancher/prometheus-auth/pkg/kube" "github.com/stretchr/testify/require" + authentication "k8s.io/api/authentication/v1" ) type ScenarioType string @@ -131,6 +132,80 @@ func getTestCases(t *testing.T) []httpTestCase { Token: "someNamespacesToken", Scenarios: samples.SomeNamespacesTokenSeriesScenarios, }, + // myToken + { + Type: FederateScenario, + HTTPMethod: http.MethodGet, + Token: "myToken", + Scenarios: samples.MyTokenFederateScenarios, + }, + { + Type: LabelScenario, + HTTPMethod: http.MethodGet, + Token: "myToken", + Scenarios: samples.MyTokenLabelScenarios, + }, + { + Type: QueryScenario, + HTTPMethod: http.MethodGet, + Token: "myToken", + Scenarios: samples.MyTokenQueryScenarios, + }, + { + Type: QueryScenario, + HTTPMethod: http.MethodPost, + Token: "myToken", + Scenarios: samples.MyTokenQueryScenarios, + }, + { + Type: ReadScenario, + HTTPMethod: http.MethodPost, + Token: "myToken", + Scenarios: samples.MyTokenReadScenarios(t), + }, + { + Type: SeriesScenario, + HTTPMethod: http.MethodGet, + Token: "myToken", + Scenarios: samples.MyTokenSeriesScenarios, + }, + // unauthenticated + { + Type: FederateScenario, + HTTPMethod: http.MethodGet, + Token: "unauthenticated", + Scenarios: samples.MyTokenFederateScenarios, + }, + { + Type: LabelScenario, + HTTPMethod: http.MethodGet, + Token: "unauthenticated", + Scenarios: samples.MyTokenLabelScenarios, + }, + { + Type: QueryScenario, + HTTPMethod: http.MethodGet, + Token: "unauthenticated", + Scenarios: samples.MyTokenQueryScenarios, + }, + { + Type: QueryScenario, + HTTPMethod: http.MethodPost, + Token: "unauthenticated", + Scenarios: samples.MyTokenQueryScenarios, + }, + { + Type: ReadScenario, + HTTPMethod: http.MethodPost, + Token: "unauthenticated", + Scenarios: samples.MyTokenReadScenarios(t), + }, + { + Type: SeriesScenario, + HTTPMethod: http.MethodGet, + Token: "unauthenticated", + Scenarios: samples.MyTokenSeriesScenarios, + }, } } @@ -318,8 +393,13 @@ func mockAgent(t *testing.T) *agent { } return &agent{ - cfg: agtCfg, + cfg: agtCfg, + userInfo: authentication.UserInfo{ + Username: "myUser", + UID: "cluster-admin", + }, namespaces: mockOwnedNamespaces(), + tokens: mockTokenAuth(), remoteAPI: promapiv1.NewAPI(promClient), } } @@ -338,6 +418,15 @@ func (v ScenarioValidator) Validate(t *testing.T, handler http.Handler) { return } + // Validate unauthenticated user + if v.Token == "unauthenticated" { + // unauthenticated user + if got := res.Code; got != http.StatusUnauthorized { + t.Errorf("[series] [GET] token %q scenario %q: got code %d, want %d for unauthenticated users", v.Token, v.Name, got, http.StatusUnauthorized) + } + return + } + // Validate response code if got, want := res.Code, v.Scenario.RespCode; got != want { t.Errorf("[series] [GET] token %q scenario %q: got code %d, want %d", v.Token, v.Name, got, want) @@ -452,6 +541,8 @@ func (v ScenarioValidator) validateProtoBody(t *testing.T, res *httptest.Respons t.Fatal(err) } + sortReadResponse(&protoRes) + if got, want := protoRes.Results, v.Scenario.RespBody; !reflect.DeepEqual(got, want) { t.Errorf("[%s] [%s] token %q scenario %q: got body\n%v\n, want\n%v\n", v.Type, v.Method, v.Token, v.Name, got, want) } @@ -491,6 +582,39 @@ func jsonResponseBody(body interface{}) string { return string(respBytes) } +type SortableTimeSeries []*prompb.TimeSeries + +func (s SortableTimeSeries) Len() int { + return len(s) +} + +func (s SortableTimeSeries) Less(i, j int) bool { + k := 0 + for k < len(s[i].Labels) && k < len(s[j].Labels) { + // compare keys + if s[i].Labels[k].Name != s[j].Labels[k].Name { + return s[i].Labels[k].Name < s[j].Labels[k].Name + } + // compare values + if s[i].Labels[k].Value != s[j].Labels[k].Value { + return s[i].Labels[k].Value < s[j].Labels[k].Value + } + k += 1 + } + // default to preserving order + return true +} + +func (s SortableTimeSeries) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func sortReadResponse(rr *prompb.ReadResponse) { + for _, q := range rr.Results { + sort.Sort(SortableTimeSeries(q.Timeseries)) + } +} + type fakeOwnedNamespaces struct { token2Namespaces map[string]data.Set } @@ -508,6 +632,37 @@ func mockOwnedNamespaces() kube.Namespaces { } } +type fakeTokenAuth struct { + token2UserInfo map[string]authentication.UserInfo +} + +func (f *fakeTokenAuth) Authenticate(token string) (authentication.UserInfo, error) { + userInfo, ok := f.token2UserInfo[token] + if !ok { + return userInfo, fmt.Errorf("user is not authenticated") + } + return userInfo, nil +} + +func mockTokenAuth() kube.Tokens { + return &fakeTokenAuth{ + token2UserInfo: map[string]authentication.UserInfo{ + "myToken": authentication.UserInfo{ + Username: "myUser", + UID: "cluster-admin", + }, + "someNamespacesToken": authentication.UserInfo{ + Username: "someNamespacesUser", + UID: "project-member", + }, + "noneNamespacesToken": authentication.UserInfo{ + Username: "noneNamespacesUser", + UID: "cluster-member", + }, + }, + } +} + type dbAdapter struct { *promtsdb.DB } diff --git a/pkg/agent/samples/federate.go b/pkg/agent/samples/federate.go index 251b52bc..e4850426 100644 --- a/pkg/agent/samples/federate.go +++ b/pkg/agent/samples/federate.go @@ -244,3 +244,166 @@ test_metric1{foo="bar",namespace="ns-a",instance="",prometheus="cluster-level/te RespBody: ``, }, } + +var MyTokenFederateScenarios = map[string]Scenario{ + "empty": { + Queries: url.Values{}, + RespCode: http.StatusOK, + RespBody: ``, + }, + "match nothing": { + Queries: url.Values{ + "match[]": []string{"does_not_match_anything"}, + }, + RespCode: http.StatusOK, + RespBody: ``, + }, + "invalid Params from the beginning": { + Queries: url.Values{ + "match[]": []string{"-not-a-valid-metric-name"}, + }, + RespCode: http.StatusBadRequest, + RespBody: `1:1: parse error: unexpected +`, + }, + "invalid Params somewhere in the middle": { + Queries: url.Values{ + "match[]": []string{"not-a-valid-metric-name"}, + }, + RespCode: http.StatusBadRequest, + RespBody: `1:4: parse error: unexpected +`, + }, + "test_metric1": { + Queries: url.Values{ + "match[]": []string{"test_metric1"}, + }, + RespCode: http.StatusOK, + RespBody: `# TYPE test_metric1 untyped +test_metric1{foo="bar",namespace="ns-a",instance="",prometheus="cluster-level/test"} 10000 6000000 +test_metric1{foo="boo",namespace="ns-c",instance="",prometheus="cluster-level/test"} 1 6000000 +`, + }, + "test_metric2": { + Queries: url.Values{ + "match[]": []string{"test_metric2"}, + }, + RespCode: http.StatusOK, + RespBody: `# TYPE test_metric2 untyped +test_metric2{foo="boo",instance="",prometheus="cluster-level/test"} 1 6000000 +`, + }, + "test_metric_without_labels": { + Queries: url.Values{ + "match[]": []string{"test_metric_without_labels"}, + }, + RespCode: http.StatusOK, + RespBody: `# TYPE test_metric_without_labels untyped +test_metric_without_labels{instance="",prometheus="cluster-level/test"} 1001 6000000 +`, + }, + "test_stale_metric": { + Queries: url.Values{ + "match[]": []string{"test_metric_stale"}, + }, + RespCode: http.StatusOK, + RespBody: ``, + }, + "test_old_metric": { + Queries: url.Values{ + "match[]": []string{"test_metric_old"}, + }, + RespCode: http.StatusOK, + RespBody: `# TYPE test_metric_old untyped +test_metric_old{instance="",prometheus="cluster-level/test"} 981 5880000 +`, + }, + "{foo='boo'}": { + Queries: url.Values{ + "match[]": []string{"{foo='boo'}"}, + }, + RespCode: http.StatusOK, + RespBody: `# TYPE test_metric1 untyped +test_metric1{foo="boo",namespace="ns-c",instance="",prometheus="cluster-level/test"} 1 6000000 +# TYPE test_metric2 untyped +test_metric2{foo="boo",instance="",prometheus="cluster-level/test"} 1 6000000 +`, + }, + "{namespace='ns-c'}": { + Queries: url.Values{ + "match[]": []string{"{namespace='ns-c'}"}, + }, + RespCode: http.StatusOK, + RespBody: `# TYPE test_metric1 untyped +test_metric1{foo="boo",namespace="ns-c",instance="",prometheus="cluster-level/test"} 1 6000000 +`, + }, + "two matchers": { + Queries: url.Values{ + "match[]": []string{"test_metric1", "test_metric2"}, + }, + RespCode: http.StatusOK, + RespBody: `# TYPE test_metric1 untyped +test_metric1{foo="bar",namespace="ns-a",instance="",prometheus="cluster-level/test"} 10000 6000000 +test_metric1{foo="boo",namespace="ns-c",instance="",prometheus="cluster-level/test"} 1 6000000 +# TYPE test_metric2 untyped +test_metric2{foo="boo",instance="",prometheus="cluster-level/test"} 1 6000000 +`, + }, + "everything": { + Queries: url.Values{ + "match[]": []string{"{__name__=~'.+'}"}, + }, + RespCode: http.StatusOK, + RespBody: `# TYPE test_metric1 untyped +test_metric1{foo="bar",namespace="ns-a",instance="",prometheus="cluster-level/test"} 10000 6000000 +test_metric1{foo="boo",namespace="ns-c",instance="",prometheus="cluster-level/test"} 1 6000000 +# TYPE test_metric2 untyped +test_metric2{foo="boo",instance="",prometheus="cluster-level/test"} 1 6000000 +# TYPE test_metric_old untyped +test_metric_old{instance="",prometheus="cluster-level/test"} 981 5880000 +# TYPE test_metric_without_labels untyped +test_metric_without_labels{instance="",prometheus="cluster-level/test"} 1001 6000000 +`, + }, + "empty existing label value matches everything that doesn't have that label": { + Queries: url.Values{ + "match[]": []string{"{foo='',__name__=~'.+'}"}, + }, + RespCode: http.StatusOK, + RespBody: `# TYPE test_metric_old untyped +test_metric_old{instance="",prometheus="cluster-level/test"} 981 5880000 +# TYPE test_metric_without_labels untyped +test_metric_without_labels{instance="",prometheus="cluster-level/test"} 1001 6000000 +`, + }, + "empty none-existing label value matches everything": { + Queries: url.Values{ + "match[]": []string{"{bar='',__name__=~'.+'}"}, + }, + RespCode: http.StatusOK, + RespBody: `# TYPE test_metric1 untyped +test_metric1{foo="bar",namespace="ns-a",instance="",prometheus="cluster-level/test"} 10000 6000000 +test_metric1{foo="boo",namespace="ns-c",instance="",prometheus="cluster-level/test"} 1 6000000 +# TYPE test_metric2 untyped +test_metric2{foo="boo",instance="",prometheus="cluster-level/test"} 1 6000000 +# TYPE test_metric_old untyped +test_metric_old{instance="",prometheus="cluster-level/test"} 981 5880000 +# TYPE test_metric_without_labels untyped +test_metric_without_labels{instance="",prometheus="cluster-level/test"} 1001 6000000 +`, + }, + "empty `namespace` label value matches everything that doesn't have `namespace` label": { + Queries: url.Values{ + "match[]": []string{"{namespace='',__name__=~'.+'}"}, + }, + RespCode: http.StatusOK, + RespBody: `# TYPE test_metric2 untyped +test_metric2{foo="boo",instance="",prometheus="cluster-level/test"} 1 6000000 +# TYPE test_metric_old untyped +test_metric_old{instance="",prometheus="cluster-level/test"} 981 5880000 +# TYPE test_metric_without_labels untyped +test_metric_without_labels{instance="",prometheus="cluster-level/test"} 1001 6000000 +`, + }, +} diff --git a/pkg/agent/samples/label.go b/pkg/agent/samples/label.go index 4807638d..5a8b834a 100644 --- a/pkg/agent/samples/label.go +++ b/pkg/agent/samples/label.go @@ -144,3 +144,79 @@ var SomeNamespacesTokenLabelScenarios = map[string]Scenario{ }, }, } + +var MyTokenLabelScenarios = map[string]Scenario{ + "bad value `invalid][query`": { + Params: map[string]string{ + "name": "invalid][query", + }, + RespCode: http.StatusBadRequest, + RespBody: &jsonResponseData{ + Status: "error", + ErrorType: "bad_data", + Error: `invalid label name: "invalid][query"`, + }, + }, + "__name__": { + Params: map[string]string{ + "name": "__name__", + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []string{ + "test_metric1", + "test_metric2", + "test_metric_old", + "test_metric_stale", + "test_metric_without_labels", + }, + }, + }, + "namespace": { + Params: map[string]string{ + "name": "namespace", + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []string{ + "ns-a", + "ns-c", + }, + }, + }, + "foo": { + Params: map[string]string{ + "name": "foo", + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []string{ + "bar", + "boo", + }, + }, + }, + "does_not_match_anything": { + Params: map[string]string{ + "name": "does_not_match_anything", + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []string{}, + }, + }, + "test_metric_without_labels": { + Params: map[string]string{ + "name": "test_metric_without_labels", + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []string{}, + }, + }, +} diff --git a/pkg/agent/samples/query.go b/pkg/agent/samples/query.go index 1925c536..67c104a6 100644 --- a/pkg/agent/samples/query.go +++ b/pkg/agent/samples/query.go @@ -709,3 +709,549 @@ var SomeNamespacesTokenQueryScenarios = map[string]Scenario{ }, }, } + +var MyTokenQueryScenarios = map[string]Scenario{ + "query - none expression with time 1": { + Endpoint: "/query", + Queries: url.Values{ + "query": []string{"2"}, + "time": []string{"123.4"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: &queryData{ + ResultType: parser.ValueTypeScalar, + Result: promql.Scalar{ + V: 2, + T: timestamp.FromTime(start.Add(123*time.Second + 400*time.Millisecond)), + }, + }, + }, + }, + "query - none expression with time 2": { + Endpoint: "/query", + Queries: url.Values{ + "query": []string{"0.333"}, + "time": []string{"1970-01-01T00:02:03Z"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: &queryData{ + ResultType: parser.ValueTypeScalar, + Result: promql.Scalar{ + V: 0.333, + T: timestamp.FromTime(start.Add(123 * time.Second)), + }, + }, + }, + }, + "query - bad query `invalid][query`": { + Endpoint: "/query", + Queries: url.Values{ + "query": []string{"invalid][query"}, + }, + RespCode: http.StatusBadRequest, + RespBody: &jsonResponseData{ + Status: "error", + ErrorType: "bad_data", + Error: `invalid parameter "query": 1:8: parse error: unexpected right bracket ']'`, + }, + }, + "query - test_metric1": { + Endpoint: "/query", + Queries: url.Values{ + "query": []string{"test_metric1"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: &queryData{ + ResultType: parser.ValueTypeVector, + Result: promql.Vector{ + promql.Sample{ + Metric: []labels.Label{ + { + Name: "__name__", + Value: "test_metric1", + }, + { + Name: "foo", + Value: "bar", + }, + { + Name: "namespace", + Value: "ns-a", + }, + }, + Point: promql.Point{ + V: 0, + T: timestamp.FromTime(start), + }, + }, + promql.Sample{ + Metric: []labels.Label{ + { + Name: "__name__", + Value: "test_metric1", + }, + { + Name: "foo", + Value: "boo", + }, + { + Name: "namespace", + Value: "ns-c", + }, + }, + Point: promql.Point{ + V: 1, + T: timestamp.FromTime(start), + }, + }, + }, + }, + }, + }, + "query - test_metric1{namespace='ns-c'}": { + Endpoint: "/query", + Queries: url.Values{ + "query": []string{"test_metric1{namespace='ns-c'}"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: &queryData{ + ResultType: parser.ValueTypeVector, + Result: promql.Vector{ + promql.Sample{ + Metric: []labels.Label{ + { + Name: "__name__", + Value: "test_metric1", + }, + { + Name: "foo", + Value: "boo", + }, + { + Name: "namespace", + Value: "ns-c", + }, + }, + Point: promql.Point{ + V: 1, + T: timestamp.FromTime(start), + }, + }, + }, + }, + }, + }, + "query - test_metric2{foo='boo'}": { + Endpoint: "/query", + Queries: url.Values{ + "query": []string{"test_metric2{foo='boo'}"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: &queryData{ + ResultType: parser.ValueTypeVector, + Result: promql.Vector{ + promql.Sample{ + Metric: []labels.Label{ + { + Name: "__name__", + Value: "test_metric2", + }, + { + Name: "foo", + Value: "boo", + }, + }, + Point: promql.Point{ + V: 1, + T: timestamp.FromTime(start), + }, + }, + }, + }, + }, + }, + "query - test_metric1[5m]": { + Endpoint: "/query", + Queries: url.Values{ + "query": []string{"test_metric1[5m]"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: &queryData{ + ResultType: parser.ValueTypeMatrix, + Result: promql.Matrix{ + promql.Series{ + Points: []promql.Point{ + {V: 0, T: timestamp.FromTime(start)}, + }, + Metric: []labels.Label{ + { + Name: "__name__", + Value: "test_metric1", + }, + { + Name: "foo", + Value: "bar", + }, + { + Name: "namespace", + Value: "ns-a", + }, + }, + }, + promql.Series{ + Points: []promql.Point{ + {V: 1, T: timestamp.FromTime(start)}, + }, + Metric: []labels.Label{ + { + Name: "__name__", + Value: "test_metric1", + }, + { + Name: "foo", + Value: "boo", + }, + { + Name: "namespace", + Value: "ns-c", + }, + }, + }, + }, + }, + }, + }, + "query - test_metric_without_labels": { + Endpoint: "/query", + Queries: url.Values{ + "query": []string{"test_metric_without_labels"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: &queryData{ + ResultType: parser.ValueTypeVector, + Result: promql.Vector{promql.Sample{ + Metric: []labels.Label{ + { + Name: "__name__", + Value: "test_metric_without_labels", + }, + }, + Point: promql.Point{ + V: 1, + T: timestamp.FromTime(start), + }, + }}, + }, + }, + }, + "query - does_not_match_anything": { + Endpoint: "/query", + Queries: url.Values{ + "query": []string{"does_not_match_anything"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: &queryData{ + ResultType: parser.ValueTypeVector, + Result: promql.Vector{}, + }, + }, + }, + "query_range - query=time()&start=0&end=2&step=1": { + Endpoint: "/query_range", + Queries: url.Values{ + "query": []string{"time()"}, + "start": []string{"0"}, + "end": []string{"2"}, + "step": []string{"1"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: &queryData{ + ResultType: parser.ValueTypeMatrix, + Result: promql.Matrix{ + promql.Series{ + Points: []promql.Point{ + {V: 0, T: timestamp.FromTime(start)}, + {V: 1, T: timestamp.FromTime(start.Add(1 * time.Second))}, + {V: 2, T: timestamp.FromTime(start.Add(2 * time.Second))}, + }, + Metric: nil, + }, + }, + }, + }, + }, + "query_range - query=time()&end=2&step=1": { + Endpoint: "/query_range", + Queries: url.Values{ + "query": []string{"time()"}, + "end": []string{"2"}, + "step": []string{"1"}, + }, + RespCode: http.StatusBadRequest, + RespBody: &jsonResponseData{ + Status: "error", + ErrorType: "bad_data", + Error: `invalid parameter "start": cannot parse "" to a valid timestamp`, + }, + }, + "query_range - bad query `invalid][query`": { + Endpoint: "/query_range", + Queries: url.Values{ + "query": []string{"invalid][query"}, + "start": []string{"0"}, + "end": []string{"100"}, + "step": []string{"1"}, + }, + RespCode: http.StatusBadRequest, + RespBody: &jsonResponseData{ + Status: "error", + ErrorType: "bad_data", + Error: `1:8: parse error: unexpected right bracket ']'`, + }, + }, + "query_range - invalid step": { + Endpoint: "/query_range", + Queries: url.Values{ + "query": []string{"time()"}, + "start": []string{"1"}, + "end": []string{"2"}, + "step": []string{"0"}, + }, + RespCode: http.StatusBadRequest, + RespBody: &jsonResponseData{ + Status: "error", + ErrorType: "bad_data", + Error: `invalid parameter "step": zero or negative query resolution step widths are not accepted. Try a positive integer`, + }, + }, + "query_range - start after end": { + Endpoint: "/query_range", + Queries: url.Values{ + "query": []string{"time()"}, + "start": []string{"2"}, + "end": []string{"1"}, + "step": []string{"1"}, + }, + RespCode: http.StatusBadRequest, + RespBody: &jsonResponseData{ + Status: "error", + ErrorType: "bad_data", + Error: `invalid parameter "end": end timestamp must not be before start time`, + }, + }, + "query_range - start overflows int64 internally": { + Endpoint: "/query_range", + Queries: url.Values{ + "query": []string{"time()"}, + "start": []string{"148966367200.372"}, + "end": []string{"1489667272.372"}, + "step": []string{"1"}, + }, + RespCode: http.StatusBadRequest, + RespBody: &jsonResponseData{ + Status: "error", + ErrorType: "bad_data", + Error: `invalid parameter "end": end timestamp must not be before start time`, + }, + }, + "query_range - test_metric1": { + Endpoint: "/query_range", + Queries: url.Values{ + "query": []string{"test_metric1"}, + "start": []string{"0"}, + "end": []string{"2"}, + "step": []string{"1"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: &queryData{ + ResultType: parser.ValueTypeMatrix, + Result: promql.Matrix{ + promql.Series{ + Metric: []labels.Label{ + { + Name: "__name__", + Value: "test_metric1", + }, + { + Name: "foo", + Value: "bar", + }, + { + Name: "namespace", + Value: "ns-a", + }, + }, + Points: []promql.Point{ + {V: 0, T: timestamp.FromTime(start)}, + {V: 0, T: timestamp.FromTime(start.Add(1 * time.Second))}, + {V: 0, T: timestamp.FromTime(start.Add(2 * time.Second))}, + }, + }, + promql.Series{ + Metric: []labels.Label{ + { + Name: "__name__", + Value: "test_metric1", + }, + { + Name: "foo", + Value: "boo", + }, + { + Name: "namespace", + Value: "ns-c", + }, + }, + Points: []promql.Point{ + {V: 1, T: timestamp.FromTime(start)}, + {V: 1, T: timestamp.FromTime(start.Add(1 * time.Second))}, + {V: 1, T: timestamp.FromTime(start.Add(2 * time.Second))}, + }, + }, + }, + }, + }, + }, + "query_range - test_metric1{namespace='ns-c'}": { + Endpoint: "/query_range", + Queries: url.Values{ + "query": []string{"test_metric1{namespace='ns-c'}"}, + "start": []string{"0"}, + "end": []string{"2"}, + "step": []string{"1"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: &queryData{ + ResultType: parser.ValueTypeMatrix, + Result: promql.Matrix{ + promql.Series{ + Metric: []labels.Label{ + { + Name: "__name__", + Value: "test_metric1", + }, + { + Name: "foo", + Value: "boo", + }, + { + Name: "namespace", + Value: "ns-c", + }, + }, + Points: []promql.Point{ + {V: 1, T: timestamp.FromTime(start)}, + {V: 1, T: timestamp.FromTime(start.Add(1 * time.Second))}, + {V: 1, T: timestamp.FromTime(start.Add(2 * time.Second))}, + }, + }, + }, + }, + }, + }, + "query_range - test_metric2{foo='boo'}": { + Endpoint: "/query_range", + Queries: url.Values{ + "query": []string{"test_metric2{foo='boo'}"}, + "start": []string{"0"}, + "end": []string{"2"}, + "step": []string{"1"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: &queryData{ + ResultType: parser.ValueTypeMatrix, + Result: promql.Matrix{ + promql.Series{ + Metric: []labels.Label{ + { + Name: "__name__", + Value: "test_metric2", + }, + { + Name: "foo", + Value: "boo", + }, + }, + Points: []promql.Point{ + {V: 1, T: timestamp.FromTime(start)}, + {V: 1, T: timestamp.FromTime(start.Add(1 * time.Second))}, + {V: 1, T: timestamp.FromTime(start.Add(2 * time.Second))}, + }, + }, + }, + }, + }, + }, + "query_range - test_metric_without_labels": { + Endpoint: "/query_range", + Queries: url.Values{ + "query": []string{"test_metric_without_labels"}, + "start": []string{"0"}, + "end": []string{"2"}, + "step": []string{"1"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: &queryData{ + ResultType: parser.ValueTypeMatrix, + Result: promql.Matrix{ + promql.Series{ + Metric: []labels.Label{ + { + Name: "__name__", + Value: "test_metric_without_labels", + }, + }, + Points: []promql.Point{ + {V: 1, T: timestamp.FromTime(start)}, + {V: 1, T: timestamp.FromTime(start.Add(1 * time.Second))}, + {V: 1, T: timestamp.FromTime(start.Add(2 * time.Second))}, + }, + }, + }, + }, + }, + }, + "query_range - does_not_match_anything": { + Endpoint: "/query", + Queries: url.Values{ + "query": []string{"does_not_match_anything"}, + "start": []string{"0"}, + "end": []string{"2"}, + "step": []string{"1"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: &queryData{ + ResultType: parser.ValueTypeVector, + Result: promql.Vector{}, + }, + }, + }, +} diff --git a/pkg/agent/samples/read.go b/pkg/agent/samples/read.go index 08d41d72..fc8eccf0 100644 --- a/pkg/agent/samples/read.go +++ b/pkg/agent/samples/read.go @@ -186,3 +186,126 @@ func SomeNamespacesTokenReadScenarios(t *testing.T) map[string]Scenario { }, } } + +func MyTokenReadScenarios(t *testing.T) map[string]Scenario { + queries := mockQueries(t, nil) + + return map[string]Scenario{ + "avg(test_metric1)": { + PrompbQueries: queries[0], + RespCode: http.StatusOK, + RespBody: []*prompb.QueryResult{ + { + Timeseries: []*prompb.TimeSeries{ + { + Labels: []prompb.Label{ + {Name: "__name__", Value: "test_metric1"}, + {Name: "foo", Value: "bar"}, + {Name: "namespace", Value: "ns-a"}, + {Name: "prometheus", Value: "cluster-level/test"}, + }, + Samples: []prompb.Sample{ + {Value: 0, Timestamp: 0}, + }, + }, + { + Labels: []prompb.Label{ + {Name: "__name__", Value: "test_metric1"}, + {Name: "foo", Value: "boo"}, + {Name: "namespace", Value: "ns-c"}, + {Name: "prometheus", Value: "cluster-level/test"}, + }, + Samples: []prompb.Sample{ + {Value: 1, Timestamp: 0}, + }, + }, + }, + }, + }, + }, + `count(test_metric1{namespace="ns-c"})`: { + PrompbQueries: queries[1], + RespCode: http.StatusOK, + RespBody: []*prompb.QueryResult{ + { + Timeseries: []*prompb.TimeSeries{ + { + Labels: []prompb.Label{ + {Name: "__name__", Value: "test_metric1"}, + {Name: "foo", Value: "boo"}, + {Name: "namespace", Value: "ns-c"}, + {Name: "prometheus", Value: "cluster-level/test"}, + }, + Samples: []prompb.Sample{ + {Value: 1, Timestamp: 0}, + }, + }, + }, + }, + }, + }, + `sum({foo="boo"})`: { + PrompbQueries: queries[2], + RespCode: http.StatusOK, + RespBody: []*prompb.QueryResult{ + { + Timeseries: []*prompb.TimeSeries{ + { + Labels: []prompb.Label{ + {Name: "__name__", Value: "test_metric1"}, + {Name: "foo", Value: "boo"}, + {Name: "namespace", Value: "ns-c"}, + {Name: "prometheus", Value: "cluster-level/test"}, + }, + Samples: []prompb.Sample{ + {Value: 1, Timestamp: 0}, + }, + }, + { + Labels: []prompb.Label{ + {Name: "__name__", Value: "test_metric2"}, + {Name: "foo", Value: "boo"}, + {Name: "prometheus", Value: "cluster-level/test"}, + }, + Samples: []prompb.Sample{ + {Value: 1, Timestamp: 0}, + }, + }, + }, + }, + }, + }, + "test_metric1[5m]": { + PrompbQueries: queries[3], + RespCode: http.StatusOK, + RespBody: []*prompb.QueryResult{ + { + Timeseries: []*prompb.TimeSeries{ + { + Labels: []prompb.Label{ + {Name: "__name__", Value: "test_metric1"}, + {Name: "foo", Value: "bar"}, + {Name: "namespace", Value: "ns-a"}, + {Name: "prometheus", Value: "cluster-level/test"}, + }, + Samples: []prompb.Sample{ + {Value: 0, Timestamp: 0}, + }, + }, + { + Labels: []prompb.Label{ + {Name: "__name__", Value: "test_metric1"}, + {Name: "foo", Value: "boo"}, + {Name: "namespace", Value: "ns-c"}, + {Name: "prometheus", Value: "cluster-level/test"}, + }, + Samples: []prompb.Sample{ + {Value: 1, Timestamp: 0}, + }, + }, + }, + }, + }, + }, + } +} diff --git a/pkg/agent/samples/series.go b/pkg/agent/samples/series.go index 1549b154..beafe56d 100644 --- a/pkg/agent/samples/series.go +++ b/pkg/agent/samples/series.go @@ -342,3 +342,191 @@ var SomeNamespacesTokenSeriesScenarios = map[string]Scenario{ }, }, } + +var MyTokenSeriesScenarios = map[string]Scenario{ + "missing match[] query Params in series requests": { + Queries: url.Values{}, + RespCode: http.StatusBadRequest, + RespBody: &jsonResponseData{ + Status: "error", + ErrorType: "bad_data", + Error: "no match[] parameter provided", + }, + }, + "bad match[] `invalid][query`": { + Queries: url.Values{ + "match[]": []string{"invalid][query"}, + }, + RespCode: http.StatusBadRequest, + RespBody: &jsonResponseData{ + Status: "error", + ErrorType: "bad_data", + Error: `invalid parameter "match[]": 1:8: parse error: unexpected right bracket ']'`, + }, + }, + "test_metric1": { + Queries: url.Values{ + "match[]": []string{"test_metric1"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []labels.Labels{ + labels.FromStrings("__name__", "test_metric1", "foo", "bar", "namespace", "ns-a"), + labels.FromStrings("__name__", "test_metric1", "foo", "boo", "namespace", "ns-c"), + }, + }, + }, + "test_metric1{namespace='ns-c'}": { + Queries: url.Values{ + "match[]": []string{"test_metric1{namespace='ns-c'}"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []labels.Labels{ + labels.FromStrings("__name__", "test_metric1", "foo", "boo", "namespace", "ns-c"), + }, + }, + }, + "test_metric2{foo='boo'}": { + Queries: url.Values{ + "match[]": []string{"test_metric2{foo='boo'}"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []labels.Labels{ + labels.FromStrings("__name__", "test_metric2", "foo", "boo"), + }, + }, + }, + "{foo='boo'}": { + Queries: url.Values{ + "match[]": []string{"{foo='boo'}"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []labels.Labels{ + labels.FromStrings("__name__", "test_metric1", "foo", "boo", "namespace", "ns-c"), + labels.FromStrings("__name__", "test_metric2", "foo", "boo"), + }, + }, + }, + "two matches": { + Queries: url.Values{ + "match[]": []string{`test_metric1{foo=~".+o$"}`, `test_metric1{foo=~".+o"}`}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []labels.Labels{ + labels.FromStrings("__name__", "test_metric1", "foo", "boo", "namespace", "ns-c"), + }, + }, + }, + "two matches, but one is `none`": { + Queries: url.Values{ + "match[]": []string{`test_metric2{foo=~".+o"}`, `none`}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []labels.Labels{ + labels.FromStrings("__name__", "test_metric2", "foo", "boo"), + }, + }, + }, + "test_metric_without_labels": { + Queries: url.Values{ + "match[]": []string{"test_metric_without_labels"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []labels.Labels{ + labels.FromStrings("__name__", "test_metric_without_labels"), + }, + }, + }, + "does_not_match_anything": { + Queries: url.Values{ + "match[]": []string{"does_not_match_anything"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []labels.Labels{}, + }, + }, + "start and end before series starts": { + Queries: url.Values{ + "match[]": []string{`test_metric1`}, + "start": []string{"-2"}, + "end": []string{"-1"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []labels.Labels{}, + }, + }, + "start and end after series ends": { + Queries: url.Values{ + "match[]": []string{`test_metric1`}, + "start": []string{"100000"}, + "end": []string{"100001"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []labels.Labels{}, + }, + }, + "start and end within series": { + Queries: url.Values{ + "match[]": []string{`test_metric1`}, + "start": []string{"1"}, + "end": []string{"100"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []labels.Labels{ + labels.FromStrings("__name__", "test_metric1", "foo", "bar", "namespace", "ns-a"), + labels.FromStrings("__name__", "test_metric1", "foo", "boo", "namespace", "ns-c"), + }, + }, + }, + "start within series, end after": { + Queries: url.Values{ + "match[]": []string{`test_metric1`}, + "start": []string{"1"}, + "end": []string{"100000"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []labels.Labels{ + labels.FromStrings("__name__", "test_metric1", "foo", "bar", "namespace", "ns-a"), + labels.FromStrings("__name__", "test_metric1", "foo", "boo", "namespace", "ns-c"), + }, + }, + }, + "start before series, end within series": { + Queries: url.Values{ + "match[]": []string{`test_metric1`}, + "start": []string{"-1"}, + "end": []string{"1"}, + }, + RespCode: http.StatusOK, + RespBody: &jsonResponseData{ + Status: "success", + Data: []labels.Labels{ + labels.FromStrings("__name__", "test_metric1", "foo", "bar", "namespace", "ns-a"), + labels.FromStrings("__name__", "test_metric1", "foo", "boo", "namespace", "ns-c"), + }, + }, + }, +} diff --git a/pkg/kube/tokens.go b/pkg/kube/tokens.go new file mode 100644 index 00000000..c809402f --- /dev/null +++ b/pkg/kube/tokens.go @@ -0,0 +1,68 @@ +package kube + +import ( + "context" + "fmt" + "time" + + authentication "k8s.io/api/authentication/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/cache" + "k8s.io/client-go/kubernetes" + clientAuthentication "k8s.io/client-go/kubernetes/typed/authentication/v1" +) + +type Tokens interface { + Authenticate(token string) (authentication.UserInfo, error) +} + +type tokens struct { + tokenReviewClient clientAuthentication.TokenReviewInterface + reviewResultTTLCache *cache.LRUExpireCache +} + +func (t *tokens) Authenticate(token string) (authentication.UserInfo, error) { + var userInfo authentication.UserInfo + + userInfoInterface, exist := t.reviewResultTTLCache.Get(token) + if exist { + userInfo = userInfoInterface.(authentication.UserInfo) + return userInfo, nil + } + + tokenReview, err := t.tokenReviewClient.Create(context.TODO(), toTokenReview(token), meta.CreateOptions{}) + if err != nil { + return userInfo, err + } + userInfo = tokenReview.Status.User + if !tokenReview.Status.Authenticated { + return userInfo, fmt.Errorf("user is not authenticated: %s", tokenReview.Status.Error) + } + t.reviewResultTTLCache.Add(token, userInfo, 5*time.Minute) + return userInfo, nil +} + +func toTokenReview(token string) *authentication.TokenReview { + return &authentication.TokenReview{ + Spec: authentication.TokenReviewSpec{ + Token: token, + }, + } +} + +func NewTokens(_ context.Context, k8sClient kubernetes.Interface) Tokens { + return &tokens{ + tokenReviewClient: k8sClient.AuthenticationV1().TokenReviews(), + reviewResultTTLCache: cache.NewLRUExpireCache(1024), + } +} + +func MatchingUsers(userInfoA, userInfoB authentication.UserInfo) bool { + if userInfoA.Username != userInfoB.Username { + return false + } + if userInfoA.UID != userInfoB.UID { + return false + } + return true +}