diff --git a/Makefile b/Makefile index 4f1cd17da7..70287dd81e 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,11 @@ else done endif +.PHONY: lint-fix +lint-fix: + MODULE=common make lint FIX_LINT=true + MODULE=k8sutils make lint FIX_LINT=true + .PHONY: build-odiglet build-odiglet: docker build -t $(ORG)/odigos-odiglet:$(TAG) . -f odiglet/Dockerfile --build-arg ODIGOS_VERSION=$(TAG) diff --git a/cli/cmd/describe.go b/cli/cmd/describe.go index eaebeb0e90..5c899e86dc 100644 --- a/cli/cmd/describe.go +++ b/cli/cmd/describe.go @@ -8,6 +8,7 @@ import ( "github.com/odigos-io/odigos/cli/cmd/resources" "github.com/odigos-io/odigos/cli/pkg/kube" cmdcontext "github.com/odigos-io/odigos/cli/pkg/cmd_context" + k8sconsts "github.com/odigos-io/odigos/k8sutils/pkg/consts" "github.com/odigos-io/odigos/k8sutils/pkg/describe" "github.com/spf13/cobra" ) @@ -141,7 +142,7 @@ var describeSourceStatefulSetCmd = &cobra.Command{ } func executeRemoteOdigosDescribe(ctx context.Context, client *kube.Client, odigosNs string) string { - uiSvcProxyEndpoint := getUiServiceOdigosEndpoint(ctx, client, odigosNs) + uiSvcProxyEndpoint := fmt.Sprintf("/api/v1/namespaces/%s/services/%s:%d/proxy/api/describe/odigos", odigosNs, k8sconsts.OdigosUiServiceName, k8sconsts.OdigosUiServicePort) request := client.Clientset.RESTClient().Get().AbsPath(uiSvcProxyEndpoint).Do(ctx) response, err := request.Raw() if err != nil { @@ -162,13 +163,6 @@ func executeRemoteSourceDescribe(ctx context.Context, client *kube.Client, workl } } -func getUiServiceOdigosEndpoint(ctx context.Context, client *kube.Client, odigosNs string) string { - uiServiceName := "ui" - uiServicePort := 3000 - - return fmt.Sprintf("/api/v1/namespaces/%s/services/%s:%d/proxy/api/describe/odigos", odigosNs, uiServiceName, uiServicePort) -} - func getUiServiceSourceEndpoint(ctx context.Context, client *kube.Client, workloadKind string, workloadNs string, workloadName string) string { ns, err := resources.GetOdigosNamespace(client, ctx) if resources.IsErrNoOdigosNamespaceFound(err) { @@ -179,10 +173,7 @@ func getUiServiceSourceEndpoint(ctx context.Context, client *kube.Client, worklo os.Exit(1) } - uiServiceName := "ui" - uiServicePort := 3000 - - return fmt.Sprintf("/api/v1/namespaces/%s/services/%s:%d/proxy/api/describe/source/namespace/%s/kind/%s/name/%s", ns, uiServiceName, uiServicePort, workloadNs, workloadKind, workloadName) + return fmt.Sprintf("/api/v1/namespaces/%s/services/%s:%d/proxy/api/describe/source/namespace/%s/kind/%s/name/%s", ns, k8sconsts.OdigosUiServiceName, k8sconsts.OdigosUiServicePort, workloadNs, workloadKind, workloadName) } func init() { diff --git a/cli/cmd/pro.go b/cli/cmd/pro.go index 775c520ff5..ef2cff9869 100644 --- a/cli/cmd/pro.go +++ b/cli/cmd/pro.go @@ -4,16 +4,17 @@ import ( "context" "fmt" "os" - "time" "github.com/odigos-io/odigos/cli/cmd/resources" cmdcontext "github.com/odigos-io/odigos/cli/pkg/cmd_context" "github.com/odigos-io/odigos/cli/pkg/kube" - "github.com/odigos-io/odigos/k8sutils/pkg/consts" - odigosconsts "github.com/odigos-io/odigos/common/consts" + k8sconsts "github.com/odigos-io/odigos/k8sutils/pkg/consts" + "github.com/odigos-io/odigos/k8sutils/pkg/pro" "github.com/spf13/cobra" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ( + updateRemoteFlag bool ) var proCmd = &cobra.Command{ @@ -32,52 +33,32 @@ var proCmd = &cobra.Command{ os.Exit(1) } onPremToken := cmd.Flag("onprem-token").Value.String() - err = updateOdigosToken(ctx, client, ns, onPremToken) + if updateRemoteFlag { + err = executeRemoteUpdateToken(ctx, client, ns, onPremToken) + } else { + err = pro.UpdateOdigosToken(ctx, client, ns, onPremToken) + } + if err != nil { fmt.Println("\033[31mERROR\033[0m Failed to update token:") fmt.Println(err) os.Exit(1) + } else { + fmt.Println() + fmt.Println("\u001B[32mSUCCESS:\u001B[0m Token updated successfully") } - - fmt.Println() - fmt.Println("\u001B[32mSUCCESS:\u001B[0m Token updated successfully") }, } -func updateOdigosToken(ctx context.Context, client *kube.Client, namespace string, onPremToken string) error { - secret, err := client.CoreV1().Secrets(namespace).Get(ctx, consts.OdigosProSecretName, metav1.GetOptions{}) - if err != nil { - if apierrors.IsNotFound(err) { - return fmt.Errorf("Tokens are not available in the open-source version of Odigos. Please contact Odigos team to inquire about pro version.") - } - return err - } - secret.Data[consts.OdigosOnpremTokenSecretKey] = []byte(onPremToken) - - _, err = client.CoreV1().Secrets(namespace).Update(ctx, secret, metav1.UpdateOptions{}) +func executeRemoteUpdateToken(ctx context.Context, client *kube.Client, namespace string, onPremToken string) error { + uiSvcProxyEndpoint := fmt.Sprintf("/api/v1/namespaces/%s/services/%s:%d/proxy/api/token/update/%s", namespace, k8sconsts.OdigosUiServiceName, k8sconsts.OdigosUiServicePort, onPremToken) + request := client.Clientset.RESTClient().Get().AbsPath(uiSvcProxyEndpoint).Do(ctx) + _, err := request.Raw() if err != nil { return err + } else { + return nil } - - daemonSet, err := client.AppsV1().DaemonSets(namespace).Get(ctx, "odiglet", metav1.GetOptions{}) - if err != nil { - return fmt.Errorf("failed to get DaemonSet odiglet in namespace %s: %v", namespace, err) - } - - // Modify the DaemonSet spec.template to trigger a rollout - if daemonSet.Spec.Template.Annotations == nil { - daemonSet.Spec.Template.Annotations = make(map[string]string) - } - daemonSet.Spec.Template.Annotations[odigosconsts.RolloutTriggerAnnotation] = time.Now().Format(time.RFC3339) - - _, err = client.AppsV1().DaemonSets(namespace).Update(ctx, daemonSet, metav1.UpdateOptions{}) - if err != nil { - fmt.Printf("Failed to restart Odiglets. Reason: %s\n", err) - fmt.Printf("To trigger a restart manually, run the following command:\n") - fmt.Printf("kubectl rollout restart daemonset odiglet -n %s\n", daemonSet.Namespace) - } - - return nil } func init() { @@ -85,4 +66,5 @@ func init() { proCmd.Flags().String("onprem-token", "", "On-prem token for Odigos") proCmd.MarkFlagRequired("onprem-token") + proCmd.PersistentFlags().BoolVarP(&updateRemoteFlag, "remote", "r", false, "use odigos ui service in the cluster to update the onprem token") } diff --git a/cli/cmd/resources/ui.go b/cli/cmd/resources/ui.go index 4aa0a13a6d..0e0dfbe00f 100644 --- a/cli/cmd/resources/ui.go +++ b/cli/cmd/resources/ui.go @@ -12,6 +12,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sconsts "github.com/odigos-io/odigos/k8sutils/pkg/consts" "github.com/odigos-io/odigos/cli/cmd/resources/resourcemanager" "github.com/odigos-io/odigos/cli/pkg/kube" @@ -288,8 +289,8 @@ func NewUIService(ns string) *corev1.Service { }, Ports: []corev1.ServicePort{ { - Name: "ui", - Port: 3000, + Name: k8sconsts.OdigosUiServiceName, + Port: k8sconsts.OdigosUiServicePort, }, { Name: "otlp", diff --git a/frontend/graph/generated.go b/frontend/graph/generated.go index a4566f9192..4dd8f2d406 100644 --- a/frontend/graph/generated.go +++ b/frontend/graph/generated.go @@ -306,6 +306,7 @@ type ComplexityRoot struct { PersistK8sNamespace func(childComplexity int, namespace model.PersistNamespaceItemInput) int PersistK8sSources func(childComplexity int, namespace string, sources []*model.PersistNamespaceSourceInput) int TestConnectionForDestination func(childComplexity int, destination model.DestinationInput) int + UpdateAPIToken func(childComplexity int, token string) int UpdateAction func(childComplexity int, id string, action model.ActionInput) int UpdateDestination func(childComplexity int, id string, destination model.DestinationInput) int UpdateInstrumentationRule func(childComplexity int, ruleID string, instrumentationRule model.InstrumentationRuleInput) int @@ -497,13 +498,14 @@ type K8sActualNamespaceResolver interface { K8sActualSources(ctx context.Context, obj *model.K8sActualNamespace, instrumentationLabeled *bool) ([]*model.K8sActualSource, error) } type MutationResolver interface { - CreateNewDestination(ctx context.Context, destination model.DestinationInput) (*model.Destination, error) + UpdateAPIToken(ctx context.Context, token string) (bool, error) PersistK8sNamespace(ctx context.Context, namespace model.PersistNamespaceItemInput) (bool, error) PersistK8sSources(ctx context.Context, namespace string, sources []*model.PersistNamespaceSourceInput) (bool, error) - TestConnectionForDestination(ctx context.Context, destination model.DestinationInput) (*model.TestConnectionResponse, error) UpdateK8sActualSource(ctx context.Context, sourceID model.K8sSourceID, patchSourceRequest model.PatchSourceRequestInput) (bool, error) + CreateNewDestination(ctx context.Context, destination model.DestinationInput) (*model.Destination, error) UpdateDestination(ctx context.Context, id string, destination model.DestinationInput) (*model.Destination, error) DeleteDestination(ctx context.Context, id string) (bool, error) + TestConnectionForDestination(ctx context.Context, destination model.DestinationInput) (*model.TestConnectionResponse, error) CreateAction(ctx context.Context, action model.ActionInput) (model.Action, error) UpdateAction(ctx context.Context, id string, action model.ActionInput) (model.Action, error) DeleteAction(ctx context.Context, id string, actionType string) (bool, error) @@ -1672,6 +1674,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.TestConnectionForDestination(childComplexity, args["destination"].(model.DestinationInput)), true + case "Mutation.updateApiToken": + if e.complexity.Mutation.UpdateAPIToken == nil { + break + } + + args, err := ec.field_Mutation_updateApiToken_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.UpdateAPIToken(childComplexity, args["token"].(string)), true + case "Mutation.updateAction": if e.complexity.Mutation.UpdateAction == nil { break @@ -2815,6 +2829,21 @@ func (ec *executionContext) field_Mutation_updateAction_args(ctx context.Context return args, nil } +func (ec *executionContext) field_Mutation_updateApiToken_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["token"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("token")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["token"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_updateDestination_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -9730,8 +9759,8 @@ func (ec *executionContext) fieldContext_MessagingPayloadCollection_dropPartialP return fc, nil } -func (ec *executionContext) _Mutation_createNewDestination(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Mutation_createNewDestination(ctx, field) +func (ec *executionContext) _Mutation_updateApiToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_updateApiToken(ctx, field) if err != nil { return graphql.Null } @@ -9744,7 +9773,7 @@ func (ec *executionContext) _Mutation_createNewDestination(ctx context.Context, }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateNewDestination(rctx, fc.Args["destination"].(model.DestinationInput)) + return ec.resolvers.Mutation().UpdateAPIToken(rctx, fc.Args["token"].(string)) }) if err != nil { ec.Error(ctx, err) @@ -9756,35 +9785,19 @@ func (ec *executionContext) _Mutation_createNewDestination(ctx context.Context, } return graphql.Null } - res := resTmp.(*model.Destination) + res := resTmp.(bool) fc.Result = res - return ec.marshalNDestination2ᚖgithubᚗcomᚋodigosᚑioᚋodigosᚋfrontendᚋgraphᚋmodelᚐDestination(ctx, field.Selections, res) + return ec.marshalNBoolean2bool(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Mutation_createNewDestination(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_updateApiToken(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, IsMethod: true, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "id": - return ec.fieldContext_Destination_id(ctx, field) - case "name": - return ec.fieldContext_Destination_name(ctx, field) - case "type": - return ec.fieldContext_Destination_type(ctx, field) - case "exportedSignals": - return ec.fieldContext_Destination_exportedSignals(ctx, field) - case "fields": - return ec.fieldContext_Destination_fields(ctx, field) - case "destinationType": - return ec.fieldContext_Destination_destinationType(ctx, field) - case "conditions": - return ec.fieldContext_Destination_conditions(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type Destination", field.Name) + return nil, errors.New("field of type Boolean does not have child fields") }, } defer func() { @@ -9794,7 +9807,7 @@ func (ec *executionContext) fieldContext_Mutation_createNewDestination(ctx conte } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_createNewDestination_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Mutation_updateApiToken_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } @@ -9911,8 +9924,8 @@ func (ec *executionContext) fieldContext_Mutation_persistK8sSources(ctx context. return fc, nil } -func (ec *executionContext) _Mutation_testConnectionForDestination(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Mutation_testConnectionForDestination(ctx, field) +func (ec *executionContext) _Mutation_updateK8sActualSource(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_updateK8sActualSource(ctx, field) if err != nil { return graphql.Null } @@ -9925,7 +9938,7 @@ func (ec *executionContext) _Mutation_testConnectionForDestination(ctx context.C }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().TestConnectionForDestination(rctx, fc.Args["destination"].(model.DestinationInput)) + return ec.resolvers.Mutation().UpdateK8sActualSource(rctx, fc.Args["sourceId"].(model.K8sSourceID), fc.Args["patchSourceRequest"].(model.PatchSourceRequestInput)) }) if err != nil { ec.Error(ctx, err) @@ -9937,31 +9950,19 @@ func (ec *executionContext) _Mutation_testConnectionForDestination(ctx context.C } return graphql.Null } - res := resTmp.(*model.TestConnectionResponse) + res := resTmp.(bool) fc.Result = res - return ec.marshalNTestConnectionResponse2ᚖgithubᚗcomᚋodigosᚑioᚋodigosᚋfrontendᚋgraphᚋmodelᚐTestConnectionResponse(ctx, field.Selections, res) + return ec.marshalNBoolean2bool(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Mutation_testConnectionForDestination(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_updateK8sActualSource(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, IsMethod: true, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "succeeded": - return ec.fieldContext_TestConnectionResponse_succeeded(ctx, field) - case "statusCode": - return ec.fieldContext_TestConnectionResponse_statusCode(ctx, field) - case "destinationType": - return ec.fieldContext_TestConnectionResponse_destinationType(ctx, field) - case "message": - return ec.fieldContext_TestConnectionResponse_message(ctx, field) - case "reason": - return ec.fieldContext_TestConnectionResponse_reason(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type TestConnectionResponse", field.Name) + return nil, errors.New("field of type Boolean does not have child fields") }, } defer func() { @@ -9971,15 +9972,15 @@ func (ec *executionContext) fieldContext_Mutation_testConnectionForDestination(c } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_testConnectionForDestination_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Mutation_updateK8sActualSource_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } return fc, nil } -func (ec *executionContext) _Mutation_updateK8sActualSource(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Mutation_updateK8sActualSource(ctx, field) +func (ec *executionContext) _Mutation_createNewDestination(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_createNewDestination(ctx, field) if err != nil { return graphql.Null } @@ -9992,7 +9993,7 @@ func (ec *executionContext) _Mutation_updateK8sActualSource(ctx context.Context, }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateK8sActualSource(rctx, fc.Args["sourceId"].(model.K8sSourceID), fc.Args["patchSourceRequest"].(model.PatchSourceRequestInput)) + return ec.resolvers.Mutation().CreateNewDestination(rctx, fc.Args["destination"].(model.DestinationInput)) }) if err != nil { ec.Error(ctx, err) @@ -10004,19 +10005,35 @@ func (ec *executionContext) _Mutation_updateK8sActualSource(ctx context.Context, } return graphql.Null } - res := resTmp.(bool) + res := resTmp.(*model.Destination) fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) + return ec.marshalNDestination2ᚖgithubᚗcomᚋodigosᚑioᚋodigosᚋfrontendᚋgraphᚋmodelᚐDestination(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Mutation_updateK8sActualSource(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_createNewDestination(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, IsMethod: true, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") + switch field.Name { + case "id": + return ec.fieldContext_Destination_id(ctx, field) + case "name": + return ec.fieldContext_Destination_name(ctx, field) + case "type": + return ec.fieldContext_Destination_type(ctx, field) + case "exportedSignals": + return ec.fieldContext_Destination_exportedSignals(ctx, field) + case "fields": + return ec.fieldContext_Destination_fields(ctx, field) + case "destinationType": + return ec.fieldContext_Destination_destinationType(ctx, field) + case "conditions": + return ec.fieldContext_Destination_conditions(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Destination", field.Name) }, } defer func() { @@ -10026,7 +10043,7 @@ func (ec *executionContext) fieldContext_Mutation_updateK8sActualSource(ctx cont } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_updateK8sActualSource_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Mutation_createNewDestination_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } @@ -10159,6 +10176,73 @@ func (ec *executionContext) fieldContext_Mutation_deleteDestination(ctx context. return fc, nil } +func (ec *executionContext) _Mutation_testConnectionForDestination(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_testConnectionForDestination(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().TestConnectionForDestination(rctx, fc.Args["destination"].(model.DestinationInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.TestConnectionResponse) + fc.Result = res + return ec.marshalNTestConnectionResponse2ᚖgithubᚗcomᚋodigosᚑioᚋodigosᚋfrontendᚋgraphᚋmodelᚐTestConnectionResponse(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_testConnectionForDestination(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "succeeded": + return ec.fieldContext_TestConnectionResponse_succeeded(ctx, field) + case "statusCode": + return ec.fieldContext_TestConnectionResponse_statusCode(ctx, field) + case "destinationType": + return ec.fieldContext_TestConnectionResponse_destinationType(ctx, field) + case "message": + return ec.fieldContext_TestConnectionResponse_message(ctx, field) + case "reason": + return ec.fieldContext_TestConnectionResponse_reason(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type TestConnectionResponse", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_testConnectionForDestination_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_createAction(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_createAction(ctx, field) if err != nil { @@ -20243,9 +20327,9 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Mutation") - case "createNewDestination": + case "updateApiToken": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_createNewDestination(ctx, field) + return ec._Mutation_updateApiToken(ctx, field) }) if out.Values[i] == graphql.Null { out.Invalids++ @@ -20264,16 +20348,16 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } - case "testConnectionForDestination": + case "updateK8sActualSource": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_testConnectionForDestination(ctx, field) + return ec._Mutation_updateK8sActualSource(ctx, field) }) if out.Values[i] == graphql.Null { out.Invalids++ } - case "updateK8sActualSource": + case "createNewDestination": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_updateK8sActualSource(ctx, field) + return ec._Mutation_createNewDestination(ctx, field) }) if out.Values[i] == graphql.Null { out.Invalids++ @@ -20292,6 +20376,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "testConnectionForDestination": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_testConnectionForDestination(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "createAction": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_createAction(ctx, field) diff --git a/frontend/graph/schema.graphqls b/frontend/graph/schema.graphqls index d0abebda6d..e2906a839a 100644 --- a/frontend/graph/schema.graphqls +++ b/frontend/graph/schema.graphqls @@ -574,13 +574,18 @@ type Query { } type Mutation { - createNewDestination(destination: DestinationInput!): Destination! + updateApiToken(token: String!): Boolean! + persistK8sNamespace(namespace: PersistNamespaceItemInput!): Boolean! + persistK8sSources(namespace: String!, sources: [PersistNamespaceSourceInput!]!): Boolean! - testConnectionForDestination(destination: DestinationInput!): TestConnectionResponse! updateK8sActualSource(sourceId: K8sSourceId!, patchSourceRequest: PatchSourceRequestInput!): Boolean! + + createNewDestination(destination: DestinationInput!): Destination! updateDestination(id: ID!, destination: DestinationInput!): Destination! deleteDestination(id: ID!): Boolean! + testConnectionForDestination(destination: DestinationInput!): TestConnectionResponse! + createAction(action: ActionInput!): Action! updateAction(id: ID!, action: ActionInput!): Action! deleteAction(id: ID!, actionType: String!): Boolean! diff --git a/frontend/graph/schema.resolvers.go b/frontend/graph/schema.resolvers.go index 1e74f8ba3b..fd2b1858d3 100644 --- a/frontend/graph/schema.resolvers.go +++ b/frontend/graph/schema.resolvers.go @@ -20,6 +20,7 @@ import ( "github.com/odigos-io/odigos/frontend/services/describe/source_describe" testconnection "github.com/odigos-io/odigos/frontend/services/test_connection" k8sconsts "github.com/odigos-io/odigos/k8sutils/pkg/consts" + "github.com/odigos-io/odigos/k8sutils/pkg/pro" "github.com/odigos-io/odigos/k8sutils/pkg/workload" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -44,7 +45,18 @@ func (r *computePlatformResolver) APITokens(ctx context.Context, obj *model.Comp // Extract the payload from the JWT tokenPayload, err := extractJWTPayload(token) if err != nil { - return nil, fmt.Errorf("failed to extract JWT payload: %w", err) + // We don't want to return an error here, because the user may have provided a bad token. + // Throwing this will prevent the entire CP from being fetched, and prevent the user from being able to update the token... + // return nil, fmt.Errorf("failed to extract JWT payload: %w", err) + + return []*model.APIToken{ + { + Token: token, + Name: "ERROR", + IssuedAt: 0, + ExpiresAt: 0, + }, + }, nil } // Extract values from the token payload @@ -53,16 +65,14 @@ func (r *computePlatformResolver) APITokens(ctx context.Context, obj *model.Comp exp, _ := tokenPayload["exp"].(float64) // We need to return an array (even if it's just 1 token), because in the future we will have to support multiple platforms. - secrets := []*model.APIToken{ + return []*model.APIToken{ { Token: token, Name: aud, IssuedAt: int(iat) * 1000, // Convert to milliseconds ExpiresAt: int(exp) * 1000, // Convert to milliseconds }, - } - - return secrets, nil + }, nil } // K8sActualNamespaces is the resolver for the k8sActualNamespaces field. @@ -330,6 +340,60 @@ func (r *k8sActualNamespaceResolver) K8sActualSources(ctx context.Context, obj * return namespaceActualSourcesPointers, nil } +// UpdateAPIToken is the resolver for the updateApiToken field. +func (r *mutationResolver) UpdateAPIToken(ctx context.Context, token string) (bool, error) { + err := pro.UpdateOdigosToken(ctx, kube.DefaultClient, consts.DefaultOdigosNamespace, token) + return err == nil, nil +} + +// PersistK8sNamespace is the resolver for the persistK8sNamespace field. +func (r *mutationResolver) PersistK8sNamespace(ctx context.Context, namespace model.PersistNamespaceItemInput) (bool, error) { + jsonMergePayload := services.GetJsonMergePatchForInstrumentationLabel(&namespace.FutureSelected) + _, err := kube.DefaultClient.CoreV1().Namespaces().Patch(ctx, namespace.Name, types.MergePatchType, jsonMergePayload, metav1.PatchOptions{}) + if err != nil { + return false, fmt.Errorf("failed to patch namespace: %v", err) + } + + return true, nil +} + +// PersistK8sSources is the resolver for the persistK8sSources field. +func (r *mutationResolver) PersistK8sSources(ctx context.Context, namespace string, sources []*model.PersistNamespaceSourceInput) (bool, error) { + var persistObjects []model.PersistNamespaceSourceInput + for _, source := range sources { + persistObjects = append(persistObjects, model.PersistNamespaceSourceInput{ + Name: source.Name, + Kind: source.Kind, + Selected: source.Selected, + }) + } + + err := services.SyncWorkloadsInNamespace(ctx, namespace, persistObjects) + if err != nil { + return false, fmt.Errorf("failed to sync workloads: %v", err) + } + + return true, nil +} + +// UpdateK8sActualSource is the resolver for the updateK8sActualSource field. +func (r *mutationResolver) UpdateK8sActualSource(ctx context.Context, sourceID model.K8sSourceID, patchSourceRequest model.PatchSourceRequestInput) (bool, error) { + ns := sourceID.Namespace + kind := string(sourceID.Kind) + name := sourceID.Name + + request := patchSourceRequest + + // Handle ReportedName update + if request.ReportedName != nil { + if err := services.UpdateReportedName(ctx, ns, kind, name, *request.ReportedName); err != nil { + return false, err + } + } + + return true, nil +} + // CreateNewDestination is the resolver for the createNewDestination field. func (r *mutationResolver) CreateNewDestination(ctx context.Context, destination model.DestinationInput) (*model.Destination, error) { odigosns := consts.DefaultOdigosNamespace @@ -401,90 +465,6 @@ func (r *mutationResolver) CreateNewDestination(ctx context.Context, destination return &endpointDest, nil } -// PersistK8sNamespace is the resolver for the persistK8sNamespace field. -func (r *mutationResolver) PersistK8sNamespace(ctx context.Context, namespace model.PersistNamespaceItemInput) (bool, error) { - jsonMergePayload := services.GetJsonMergePatchForInstrumentationLabel(&namespace.FutureSelected) - _, err := kube.DefaultClient.CoreV1().Namespaces().Patch(ctx, namespace.Name, types.MergePatchType, jsonMergePayload, metav1.PatchOptions{}) - if err != nil { - return false, fmt.Errorf("failed to patch namespace: %v", err) - } - - return true, nil -} - -// PersistK8sSources is the resolver for the persistK8sSources field. -func (r *mutationResolver) PersistK8sSources(ctx context.Context, namespace string, sources []*model.PersistNamespaceSourceInput) (bool, error) { - var persistObjects []model.PersistNamespaceSourceInput - for _, source := range sources { - persistObjects = append(persistObjects, model.PersistNamespaceSourceInput{ - Name: source.Name, - Kind: source.Kind, - Selected: source.Selected, - }) - } - - err := services.SyncWorkloadsInNamespace(ctx, namespace, persistObjects) - if err != nil { - return false, fmt.Errorf("failed to sync workloads: %v", err) - } - - return true, nil -} - -// TestConnectionForDestination is the resolver for the testConnectionForDestination field. -func (r *mutationResolver) TestConnectionForDestination(ctx context.Context, destination model.DestinationInput) (*model.TestConnectionResponse, error) { - destType := common.DestinationType(destination.Type) - - destConfig, err := services.GetDestinationTypeConfig(destType) - if err != nil { - return nil, err - } - - if !destConfig.Spec.TestConnectionSupported { - return nil, fmt.Errorf("destination type %s does not support test connection", destination.Type) - } - - configurer, err := testconnection.ConvertDestinationToConfigurer(destination) - if err != nil { - return nil, err - } - - res := testconnection.TestConnection(ctx, configurer) - if !res.Succeeded { - return &model.TestConnectionResponse{ - Succeeded: false, - StatusCode: res.StatusCode, - DestinationType: (*string)(&res.DestinationType), - Message: &res.Message, - Reason: (*string)(&res.Reason), - }, nil - } - - return &model.TestConnectionResponse{ - Succeeded: true, - StatusCode: 200, - DestinationType: (*string)(&res.DestinationType), - }, nil -} - -// UpdateK8sActualSource is the resolver for the updateK8sActualSource field. -func (r *mutationResolver) UpdateK8sActualSource(ctx context.Context, sourceID model.K8sSourceID, patchSourceRequest model.PatchSourceRequestInput) (bool, error) { - ns := sourceID.Namespace - kind := string(sourceID.Kind) - name := sourceID.Name - - request := patchSourceRequest - - // Handle ReportedName update - if request.ReportedName != nil { - if err := services.UpdateReportedName(ctx, ns, kind, name, *request.ReportedName); err != nil { - return false, err - } - } - - return true, nil -} - // UpdateDestination is the resolver for the updateDestination field. func (r *mutationResolver) UpdateDestination(ctx context.Context, id string, destination model.DestinationInput) (*model.Destination, error) { odigosns := consts.DefaultOdigosNamespace @@ -602,6 +582,42 @@ func (r *mutationResolver) DeleteDestination(ctx context.Context, id string) (bo return true, nil } +// TestConnectionForDestination is the resolver for the testConnectionForDestination field. +func (r *mutationResolver) TestConnectionForDestination(ctx context.Context, destination model.DestinationInput) (*model.TestConnectionResponse, error) { + destType := common.DestinationType(destination.Type) + + destConfig, err := services.GetDestinationTypeConfig(destType) + if err != nil { + return nil, err + } + + if !destConfig.Spec.TestConnectionSupported { + return nil, fmt.Errorf("destination type %s does not support test connection", destination.Type) + } + + configurer, err := testconnection.ConvertDestinationToConfigurer(destination) + if err != nil { + return nil, err + } + + res := testconnection.TestConnection(ctx, configurer) + if !res.Succeeded { + return &model.TestConnectionResponse{ + Succeeded: false, + StatusCode: res.StatusCode, + DestinationType: (*string)(&res.DestinationType), + Message: &res.Message, + Reason: (*string)(&res.Reason), + }, nil + } + + return &model.TestConnectionResponse{ + Succeeded: true, + StatusCode: 200, + DestinationType: (*string)(&res.DestinationType), + }, nil +} + // CreateAction is the resolver for the createAction field. func (r *mutationResolver) CreateAction(ctx context.Context, action model.ActionInput) (model.Action, error) { switch action.Type { diff --git a/frontend/main.go b/frontend/main.go index d89ba23800..cbbf16f12d 100644 --- a/frontend/main.go +++ b/frontend/main.go @@ -14,29 +14,24 @@ import ( "sync" "syscall" - "github.com/go-logr/logr" + _ "net/http/pprof" + "github.com/99designs/gqlgen/graphql/handler" + "github.com/99designs/gqlgen/graphql/playground" + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "github.com/go-logr/logr" "github.com/odigos-io/odigos/common" "github.com/odigos-io/odigos/common/consts" "github.com/odigos-io/odigos/destinations" - "github.com/odigos-io/odigos/k8sutils/pkg/env" - - "github.com/gin-contrib/cors" - + "github.com/odigos-io/odigos/frontend/graph" "github.com/odigos-io/odigos/frontend/kube" "github.com/odigos-io/odigos/frontend/kube/watchers" "github.com/odigos-io/odigos/frontend/services" collectormetrics "github.com/odigos-io/odigos/frontend/services/collector_metrics" "github.com/odigos-io/odigos/frontend/services/sse" "github.com/odigos-io/odigos/frontend/version" - - "github.com/gin-gonic/gin" - - _ "net/http/pprof" - - "github.com/99designs/gqlgen/graphql/handler" - "github.com/99designs/gqlgen/graphql/playground" - "github.com/odigos-io/odigos/frontend/graph" + "github.com/odigos-io/odigos/k8sutils/pkg/env" ) const ( @@ -109,17 +104,23 @@ func startHTTPServer(flags *Flags, odigosMetrics *collectormetrics.OdigosMetrics MetricsConsumer: odigosMetrics, }, })) - r.POST("/graphql", func(c *gin.Context) { gqlHandler.ServeHTTP(c.Writer, c.Request) }) r.GET("/playground", gin.WrapH(playground.Handler("GraphQL Playground", "/graphql"))) + // SSE handler + r.GET("/api/events", sse.HandleSSEConnections) + + // Remote CLI handlers + r.GET("/token/update/:onPremToken", services.UpdateToken) + r.GET("/describe/odigos", services.DescribeOdigos) + r.GET("/describe/source/namespace/:namespace/kind/:kind/name/:name", services.DescribeSource) + return r, nil } func httpFileServerWith404(fs http.FileSystem) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := fs.Open(r.URL.Path) if err != nil { @@ -135,6 +136,25 @@ func httpFileServerWith404(fs http.FileSystem) http.Handler { }) } +func startWatchers(ctx context.Context, flags *Flags) error { + err := watchers.StartInstrumentationConfigWatcher(ctx, "") + if err != nil { + return fmt.Errorf("error starting InstrumentationConfig watcher: %v", err) + } + + err = watchers.StartDestinationWatcher(ctx, flags.Namespace) + if err != nil { + return fmt.Errorf("error starting Destination watcher: %v", err) + } + + err = watchers.StartInstrumentationInstanceWatcher(ctx, "") + if err != nil { + return fmt.Errorf("error starting InstrumentationInstance watcher: %v", err) + } + + return nil +} + func main() { flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) flags := parseFlags() @@ -181,28 +201,11 @@ func main() { } // Start watchers - err = watchers.StartInstrumentationConfigWatcher(ctx, "") - if err != nil { - log.Printf("Error starting InstrumentationConfig watcher: %v", err) - } - - err = watchers.StartDestinationWatcher(ctx, flags.Namespace) - if err != nil { - log.Printf("Error starting Destination watcher: %v", err) - } - - err = watchers.StartInstrumentationInstanceWatcher(ctx, "") + err = startWatchers(ctx, &flags) if err != nil { - log.Printf("Error starting InstrumentationInstance watcher: %v", err) + log.Fatalf("Error starting watchers: %s", err) } - r.GET("/api/events", sse.HandleSSEConnections) - r.GET("/describe/odigos", func(c *gin.Context) { - services.DescribeOdigos(c) - }) - r.GET("/describe/source/namespace/:namespace/kind/:kind/name/:name", func(c *gin.Context) { - services.DescribeSource(c, c.Param("namespace"), c.Param("kind"), c.Param("name")) - }) log.Printf("Odigos UI is available at: http://%s:%d", flags.Address, flags.Port) go func() { err = r.Run(fmt.Sprintf("%s:%d", flags.Address, flags.Port)) diff --git a/frontend/services/describe.go b/frontend/services/describe.go index 0f06d8a740..0761aff5d9 100644 --- a/frontend/services/describe.go +++ b/frontend/services/describe.go @@ -34,8 +34,13 @@ func DescribeOdigos(c *gin.Context) { } } -func DescribeSource(c *gin.Context, ns string, kind string, name string) { +func DescribeSource(c *gin.Context) { ctx := c.Request.Context() + + ns := c.Param("namespace") + name := c.Param("name") + kind := c.Param("kind") + var desc *source.SourceAnalyze var err error switch kind { diff --git a/frontend/services/pro.go b/frontend/services/pro.go new file mode 100644 index 0000000000..f1462ad8d4 --- /dev/null +++ b/frontend/services/pro.go @@ -0,0 +1,29 @@ +package services + +import ( + "github.com/gin-gonic/gin" + "github.com/odigos-io/odigos/frontend/kube" + "github.com/odigos-io/odigos/k8sutils/pkg/env" + "github.com/odigos-io/odigos/k8sutils/pkg/pro" +) + +func UpdateToken(c *gin.Context) { + ctx := c.Request.Context() + + err := pro.UpdateOdigosToken(ctx, kube.DefaultClient, env.GetCurrentNamespace(), c.Param("onPremToken")) + if err != nil { + c.JSON(500, gin.H{ + "message": err.Error(), + }) + return + } + + statusCode := 200 + acceptHeader := c.GetHeader("Accept") + + if acceptHeader == "application/json" { + c.JSON(statusCode, struct{}{}) + } else { + c.String(statusCode, "") + } +} diff --git a/frontend/webapp/components/overview/all-drawers/cli-drawer.tsx b/frontend/webapp/components/overview/all-drawers/cli-drawer.tsx index e2731bb88c..fe2772d159 100644 --- a/frontend/webapp/components/overview/all-drawers/cli-drawer.tsx +++ b/frontend/webapp/components/overview/all-drawers/cli-drawer.tsx @@ -1,12 +1,13 @@ -import React, { useState } from 'react'; -import { FlexRow } from '@/styles'; +import React, { useRef, useState } from 'react'; +import theme from '@/styles/theme'; import styled from 'styled-components'; import { NOTIFICATION_TYPE } from '@/types'; +import { FlexColumn, FlexRow } from '@/styles'; import { DATA_CARDS, getStatusIcon, safeJsonStringify } from '@/utils'; import OverviewDrawer from '@/containers/main/overview/overview-drawer'; -import { CodeBracketsIcon, CodeIcon, CopyIcon, KeyIcon, ListIcon } from '@/assets'; -import { useComputePlatform, useCopy, useDescribeOdigos, useTimeAgo } from '@/hooks'; -import { DataCard, DataCardFieldTypes, IconButton, Segment } from '@/reuseable-components'; +import { useCopy, useDescribeOdigos, useKeyDown, useOnClickOutside, useTimeAgo, useTokenCRUD } from '@/hooks'; +import { CheckIcon, CodeBracketsIcon, CodeIcon, CopyIcon, CrossIcon, EditIcon, KeyIcon, ListIcon } from '@/assets'; +import { Button, DataCard, DataCardFieldTypes, Divider, IconButton, Input, Segment, Text, Tooltip } from '@/reuseable-components'; interface Props {} @@ -16,15 +17,50 @@ const DataContainer = styled.div` gap: 12px; `; +const Relative = styled.div` + position: relative; +`; + +const TokenPopover = styled(FlexColumn)` + position: absolute; + top: 32px; + right: 0; + z-index: 1; + gap: 8px; + padding: 24px; + background-color: ${({ theme }) => theme.colors.info}; + border: 1px solid ${({ theme }) => theme.colors.border}; + border-radius: 24px; +`; + +const PopoverFormWrapper = styled(FlexRow)` + width: 100%; +`; + +const PopoverFormButton = styled(Button)` + width: 36px; + padding-left: 0; + padding-right: 0; +`; + export const CliDrawer: React.FC = () => { const timeAgo = useTimeAgo(); - const { data: cp } = useComputePlatform(); const { isCopied, copiedIndex, clickCopy } = useCopy(); + const { tokens, loading, updateToken } = useTokenCRUD(); const { data: describe, restructureForPrettyMode } = useDescribeOdigos(); const [isPrettyMode, setIsPrettyMode] = useState(true); + const [editTokenIndex, setEditTokenIndex] = useState(-1); + + const tokenPopoverRef = useRef(null); + const tokenInputRef = useRef(null); + useOnClickOutside(tokenPopoverRef, () => setEditTokenIndex(-1)); + useKeyDown({ key: 'Enter', active: editTokenIndex !== -1 }, saveToken); - const tokens = cp?.computePlatform.apiTokens || []; + function saveToken() { + const token = tokenInputRef.current?.value; + if (token) updateToken(token).then(() => setEditTokenIndex(-1)); + } return ( @@ -59,12 +95,32 @@ export const CliDrawer: React.FC = () => { clickCopy(token, idx)}> {isCopied && copiedIndex === idx ? : } + - {/* + + setEditTokenIndex(idx)}> + + - {}}> - - */} + {editTokenIndex === idx && ( + + + + Enter a new API Token: + + + + + + + + setEditTokenIndex(-1)}> + + + + + )} + ); }, diff --git a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx index 7dd4e68534..e343adcf86 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx @@ -41,7 +41,7 @@ const OverviewDrawer: React.FC = ({ children, title, const { addNotification } = useNotificationStore(); const { selectedItem, setSelectedItem } = useDrawerStore(); - useKeyDown({ key: 'Enter', active: !!selectedItem }, () => (isEdit ? clickSave() : closeDrawer())); + useKeyDown({ key: 'Enter', active: !!selectedItem?.item }, () => (isEdit ? clickSave() : closeDrawer())); const { sources } = useSourceCRUD(); const { destinations } = useDestinationCRUD(); diff --git a/frontend/webapp/graphql/mutations/index.ts b/frontend/webapp/graphql/mutations/index.ts index 9255507746..5e662d32d5 100644 --- a/frontend/webapp/graphql/mutations/index.ts +++ b/frontend/webapp/graphql/mutations/index.ts @@ -1,5 +1,7 @@ -export * from './destination'; -export * from './source'; -export * from './namespace'; export * from './action'; +export * from './destination'; export * from './instrumentation-rule'; +export * from './metrics'; +export * from './namespace'; +export * from './source'; +export * from './token'; diff --git a/frontend/webapp/graphql/mutations/token.ts b/frontend/webapp/graphql/mutations/token.ts new file mode 100644 index 0000000000..2dbade14ae --- /dev/null +++ b/frontend/webapp/graphql/mutations/token.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const UPDATE_API_TOKEN = gql` + mutation UpdateApiToken($token: String!) { + updateApiToken(token: $token) + } +`; diff --git a/frontend/webapp/hooks/compute-platform/useComputePlatform.ts b/frontend/webapp/hooks/compute-platform/useComputePlatform.ts index e48a3b3917..126f74c4f0 100644 --- a/frontend/webapp/hooks/compute-platform/useComputePlatform.ts +++ b/frontend/webapp/hooks/compute-platform/useComputePlatform.ts @@ -1,8 +1,7 @@ import { useMemo } from 'react'; import { useQuery } from '@apollo/client'; -import { useNotificationStore } from '@/store'; import { GET_COMPUTE_PLATFORM } from '@/graphql'; -import { useFilterStore } from '@/store/useFilterStore'; +import { useFilterStore, useNotificationStore } from '@/store'; import { ACTION, deriveTypeFromRule, safeJsonParse } from '@/utils'; import { NOTIFICATION_TYPE, SupportedSignals, type ActionItem, type ComputePlatform, type ComputePlatformMapped } from '@/types'; diff --git a/frontend/webapp/hooks/index.ts b/frontend/webapp/hooks/index.ts index 0996541eab..ed7d79ade0 100644 --- a/frontend/webapp/hooks/index.ts +++ b/frontend/webapp/hooks/index.ts @@ -8,3 +8,4 @@ export * from './instrumentation-rules'; export * from './notification'; export * from './overview'; export * from './sources'; +export * from './tokens'; diff --git a/frontend/webapp/hooks/tokens/index.ts b/frontend/webapp/hooks/tokens/index.ts new file mode 100644 index 0000000000..8c363c5b02 --- /dev/null +++ b/frontend/webapp/hooks/tokens/index.ts @@ -0,0 +1 @@ +export * from './useTokenCRUD'; diff --git a/frontend/webapp/hooks/tokens/useTokenCRUD.ts b/frontend/webapp/hooks/tokens/useTokenCRUD.ts new file mode 100644 index 0000000000..3caa9fb4a7 --- /dev/null +++ b/frontend/webapp/hooks/tokens/useTokenCRUD.ts @@ -0,0 +1,53 @@ +import { useMutation } from '@apollo/client'; +import { useNotificationStore } from '@/store'; +import { ACTION, getSseTargetFromId } from '@/utils'; +import { UPDATE_API_TOKEN } from '@/graphql/mutations'; +import { useComputePlatform } from '../compute-platform'; +import { NOTIFICATION_TYPE, OVERVIEW_ENTITY_TYPES } from '@/types'; + +interface UseTokenCrudParams { + onSuccess?: (type: string) => void; + onError?: (type: string) => void; +} + +export const useTokenCRUD = (params?: UseTokenCrudParams) => { + const { data, refetch } = useComputePlatform(); + const { addNotification } = useNotificationStore(); + + const notifyUser = (type: NOTIFICATION_TYPE, title: string, message: string, id?: string, hideFromHistory?: boolean) => { + addNotification({ + type, + title, + message, + crdType: OVERVIEW_ENTITY_TYPES.ACTION, + target: id ? getSseTargetFromId(id, OVERVIEW_ENTITY_TYPES.ACTION) : undefined, + hideFromHistory, + }); + }; + + const handleError = (actionType: string, message: string) => { + notifyUser(NOTIFICATION_TYPE.ERROR, actionType, message); + params?.onError?.(actionType); + }; + + const handleComplete = (actionType: string, message: string, id?: string) => { + notifyUser(NOTIFICATION_TYPE.SUCCESS, actionType, message, id); + refetch(); + params?.onSuccess?.(actionType); + }; + + const [updateToken, uState] = useMutation<{ updateApiToken: boolean }>(UPDATE_API_TOKEN, { + onError: (error) => handleError(error.name || ACTION.UPDATE, error.cause?.message || error.message), + onCompleted: () => { + handleComplete(ACTION.UPDATE, 'API Token updated'); + refetch(); + }, + }); + + return { + loading: uState.loading, + tokens: data?.computePlatform?.apiTokens || [], + + updateToken: async (token: string) => await updateToken({ variables: { token } }), + }; +}; diff --git a/frontend/webapp/store/useNotificationStore.ts b/frontend/webapp/store/useNotificationStore.ts index d16485b3a6..8fa4173356 100644 --- a/frontend/webapp/store/useNotificationStore.ts +++ b/frontend/webapp/store/useNotificationStore.ts @@ -12,25 +12,31 @@ interface StoreState { removeNotifications: (target: string) => void; } -export const useNotificationStore = create((set) => ({ +export const useNotificationStore = create((set, get) => ({ notifications: [], addNotification: (notif) => { const date = new Date(); const id = `${date.getTime().toString()}${!!notif.target ? `#${notif.target}` : ''}`; - set((state) => ({ - notifications: [ - { - ...notif, - id, - time: date.toISOString(), - dismissed: false, - seen: false, - }, - ...state.notifications, - ], - })); + // This is to prevent duplicate notifications within a 10 second time-frame. + // This is useful for notifications that are triggered multiple times in a short period, like failed API queries... + const foundThisNotif = !!get().notifications.find((n) => n.type === notif.type && n.title === notif.title && n.message === notif.message && date.getTime() - new Date(n.time).getTime() <= 10000); // 10 seconds + + if (!foundThisNotif) { + set((state) => ({ + notifications: [ + { + ...notif, + id, + time: date.toISOString(), + dismissed: false, + seen: false, + }, + ...state.notifications, + ], + })); + } }, markAsDismissed: (id) => { diff --git a/k8sutils/pkg/consts/consts.go b/k8sutils/pkg/consts/consts.go index 08330b74d7..82eb639f2d 100644 --- a/k8sutils/pkg/consts/consts.go +++ b/k8sutils/pkg/consts/consts.go @@ -82,3 +82,8 @@ const ( OdigosCloudApiKeySecretKey = "odigos-cloud-api-key" OdigosOnpremTokenSecretKey = "odigos-onprem-token" ) + +const ( + OdigosUiServiceName = "ui" + OdigosUiServicePort = 3000 +) diff --git a/k8sutils/pkg/pro/common.go b/k8sutils/pkg/pro/common.go new file mode 100644 index 0000000000..b8c0b48fc2 --- /dev/null +++ b/k8sutils/pkg/pro/common.go @@ -0,0 +1,62 @@ +package pro + +import ( + "context" + "fmt" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + + odigosconsts "github.com/odigos-io/odigos/common/consts" + "github.com/odigos-io/odigos/k8sutils/pkg/consts" +) + +func UpdateOdigosToken(ctx context.Context, client kubernetes.Interface, namespace string, onPremToken string) error { + if err := updateSecretToken(ctx, client, namespace, onPremToken); err != nil { + return fmt.Errorf("failed to update secret token: %w", err) + } + if err := odigletRolloutTrigger(ctx, client, namespace); err != nil { + return fmt.Errorf("failed to trigger odiglet rollout: %w", err) + } + return nil +} + +func updateSecretToken(ctx context.Context, client kubernetes.Interface, namespace string, onPremToken string) error { + secret, err := client.CoreV1().Secrets(namespace).Get(ctx, consts.OdigosProSecretName, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + return fmt.Errorf("tokens are not available in the open-source version of Odigos. Please contact Odigos team to inquire about pro version") + } + return err + } + secret.Data[consts.OdigosOnpremTokenSecretKey] = []byte(onPremToken) + + _, err = client.CoreV1().Secrets(namespace).Update(ctx, secret, metav1.UpdateOptions{}) + if err != nil { + return err + } + + return nil +} + +func odigletRolloutTrigger(ctx context.Context, client kubernetes.Interface, namespace string) error { + daemonSet, err := client.AppsV1().DaemonSets(namespace).Get(ctx, "odiglet", metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("\033[31mERROR\033[0m failed to get odiglet DaemonSet in namespace %s: %v", namespace, err) + } + + // Modify the DaemonSet spec.template to trigger a rollout + if daemonSet.Spec.Template.Annotations == nil { + daemonSet.Spec.Template.Annotations = make(map[string]string) + } + daemonSet.Spec.Template.Annotations[odigosconsts.RolloutTriggerAnnotation] = time.Now().Format(time.RFC3339) + + _, err = client.AppsV1().DaemonSets(namespace).Update(ctx, daemonSet, metav1.UpdateOptions{}) + if err != nil { + command := fmt.Sprintf("kubectl rollout restart daemonset odiglet -n %s", daemonSet.Namespace) + return fmt.Errorf("failed to restart Odiglets: %w. To trigger a restart manually, run the following command: %s", err, command) + } + return nil +}