forked from wso2/agent-manager
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathagent_manager.go
More file actions
2436 lines (2170 loc) · 108 KB
/
agent_manager.go
File metadata and controls
2436 lines (2170 loc) · 108 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you 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 services
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"github.com/google/uuid"
observabilitysvc "github.com/wso2/agent-manager/agent-manager-service/clients/observabilitysvc"
"github.com/wso2/agent-manager/agent-manager-service/clients/openchoreosvc/client"
"github.com/wso2/agent-manager/agent-manager-service/clients/openchoreosvc/gen"
"github.com/wso2/agent-manager/agent-manager-service/clients/secretmanagersvc"
"github.com/wso2/agent-manager/agent-manager-service/config"
"github.com/wso2/agent-manager/agent-manager-service/middleware/jwtassertion"
"github.com/wso2/agent-manager/agent-manager-service/models"
"github.com/wso2/agent-manager/agent-manager-service/repositories"
"github.com/wso2/agent-manager/agent-manager-service/spec"
"github.com/wso2/agent-manager/agent-manager-service/utils"
)
type AgentManagerService interface {
ListAgents(ctx context.Context, orgName string, projName string, limit int32, offset int32) ([]*models.AgentResponse, int32, error)
CreateAgent(ctx context.Context, orgName string, projectName string, req *spec.CreateAgentRequest) error
UpdateAgentBasicInfo(ctx context.Context, orgName string, projectName string, agentName string, req *spec.UpdateAgentBasicInfoRequest) (*models.AgentResponse, error)
UpdateAgentBuildParameters(ctx context.Context, orgName string, projectName string, agentName string, req *spec.UpdateAgentBuildParametersRequest) (*models.AgentResponse, error)
BuildAgent(ctx context.Context, orgName string, projectName string, agentName string, commitId string) (*models.BuildResponse, error)
DeleteAgent(ctx context.Context, orgName string, projectName string, agentName string) error
DeployAgent(ctx context.Context, orgName string, projectName string, agentName string, req *spec.DeployAgentRequest) (string, error)
GetAgent(ctx context.Context, orgName string, projectName string, agentName string) (*models.AgentResponse, error)
ListAgentBuilds(ctx context.Context, orgName string, projectName string, agentName string, limit int32, offset int32) ([]*models.BuildResponse, int32, error)
GetBuild(ctx context.Context, orgName string, projectName string, agentName string, buildName string) (*models.BuildDetailsResponse, error)
GetAgentDeployments(ctx context.Context, orgName string, projectName string, agentName string) ([]*models.DeploymentResponse, error)
UpdateAgentDeploymentState(ctx context.Context, orgName string, projectName string, agentName string, environment string, state string) error
GetAgentEndpoints(ctx context.Context, orgName string, projectName string, agentName string, environmentName string) (map[string]models.EndpointsResponse, error)
GetAgentConfigurations(ctx context.Context, orgName string, projectName string, agentName string, environment string) ([]models.EnvVars, error)
GetBuildLogs(ctx context.Context, orgName string, projectName string, agentName string, buildName string) (*models.LogsResponse, error)
GenerateName(ctx context.Context, orgName string, payload spec.ResourceNameRequest) (string, error)
GetAgentMetrics(ctx context.Context, orgName string, projectName string, agentName string, payload spec.MetricsFilterRequest) (*spec.MetricsResponse, error)
GetAgentRuntimeLogs(ctx context.Context, orgName string, projectName string, agentName string, payload spec.LogFilterRequest) (*models.LogsResponse, error)
GetAgentResourceConfigs(ctx context.Context, orgName string, projectName string, agentName string, environment string) (*spec.AgentResourceConfigsResponse, error)
UpdateAgentResourceConfigs(ctx context.Context, orgName string, projectName string, agentName string, environment string, req *spec.UpdateAgentResourceConfigsRequest) (*spec.AgentResourceConfigsResponse, error)
}
type agentManagerService struct {
ocClient client.OpenChoreoClient
observabilitySvcClient observabilitysvc.ObservabilitySvcClient
secretMgmtClient secretmanagersvc.SecretManagementClient
gitRepositoryService RepositoryService
tokenManagerService AgentTokenManagerService
agentConfigRepo repositories.AgentConfigRepository
agentConfigurationService AgentConfigurationService
logger *slog.Logger
}
func NewAgentManagerService(
OpenChoreoClient client.OpenChoreoClient,
observabilitySvcClient observabilitysvc.ObservabilitySvcClient,
secretMgmtClient secretmanagersvc.SecretManagementClient,
gitRepositoryService RepositoryService,
tokenManagerService AgentTokenManagerService,
agentConfigRepo repositories.AgentConfigRepository,
agentConfigurationService AgentConfigurationService,
logger *slog.Logger,
) AgentManagerService {
return &agentManagerService{
ocClient: OpenChoreoClient,
observabilitySvcClient: observabilitySvcClient,
secretMgmtClient: secretMgmtClient,
gitRepositoryService: gitRepositoryService,
tokenManagerService: tokenManagerService,
agentConfigRepo: agentConfigRepo,
agentConfigurationService: agentConfigurationService,
logger: logger,
}
}
// -----------------------------------------------------------------------------
// Error Translation Helpers
// -----------------------------------------------------------------------------
// translateOrgError translates a generic ErrNotFound to ErrOrganizationNotFound
func translateOrgError(err error) error {
if err != nil && errors.Is(err, utils.ErrNotFound) {
return utils.ErrOrganizationNotFound
}
return err
}
// translateProjectError translates a generic ErrNotFound to ErrProjectNotFound
func translateProjectError(err error) error {
if err != nil && errors.Is(err, utils.ErrNotFound) {
return utils.ErrProjectNotFound
}
return err
}
// translateAgentError translates a generic ErrNotFound to ErrAgentNotFound
func translateAgentError(err error) error {
if err != nil && errors.Is(err, utils.ErrNotFound) {
return utils.ErrAgentNotFound
}
return err
}
// translateBuildError translates a generic ErrNotFound to ErrBuildNotFound
func translateBuildError(err error) error {
if err != nil && errors.Is(err, utils.ErrNotFound) {
return utils.ErrBuildNotFound
}
return err
}
// translateEnvironmentError translates a generic ErrNotFound to ErrEnvironmentNotFound
func translateEnvironmentError(err error) error {
if err != nil && errors.Is(err, utils.ErrNotFound) {
return utils.ErrEnvironmentNotFound
}
return err
}
// translatePipelineError translates a generic ErrNotFound to ErrDeploymentPipelineNotFound
func translatePipelineError(err error) error {
if err != nil && errors.Is(err, utils.ErrNotFound) {
return utils.ErrDeploymentPipelineNotFound
}
return err
}
// validateGitSecretExists checks if the specified git secret exists in the organization
func (s *agentManagerService) validateGitSecretExists(ctx context.Context, orgName string, secretRef string) error {
if secretRef == "" {
return fmt.Errorf("git secret reference is empty")
}
secrets, err := s.ocClient.ListGitSecrets(ctx, orgName)
if err != nil {
s.logger.Error("Failed to list git secrets for validation", "orgName", orgName, "error", err)
return fmt.Errorf("failed to validate git secret: %w", err)
}
for _, secret := range secrets {
if secret.Name == secretRef {
return nil
}
}
s.logger.Error("Git secret not found", "orgName", orgName, "secretRef", secretRef)
return utils.ErrGitSecretNotFound
}
// Build type constants
const (
BuildTypeBuildpack = "buildpack"
BuildTypeDocker = "docker"
)
// -----------------------------------------------------------------------------
// Mapping Helper Functions
// -----------------------------------------------------------------------------
// mapBuildConfig converts spec.Build to client.BuildConfig
func mapBuildConfig(specBuild *spec.Build) *client.BuildConfig {
if specBuild == nil {
return nil
}
if specBuild.BuildpackBuild != nil {
return &client.BuildConfig{
Type: BuildTypeBuildpack,
Buildpack: &client.BuildpackConfig{
Language: specBuild.BuildpackBuild.Buildpack.Language,
LanguageVersion: utils.StrPointerAsStr(specBuild.BuildpackBuild.Buildpack.LanguageVersion, ""),
RunCommand: utils.StrPointerAsStr(specBuild.BuildpackBuild.Buildpack.RunCommand, ""),
},
}
}
if specBuild.DockerBuild != nil {
return &client.BuildConfig{
Type: BuildTypeDocker,
Docker: &client.DockerConfig{
DockerfilePath: specBuild.DockerBuild.Docker.DockerfilePath,
},
}
}
return nil
}
// mapConfigurationsWithSecrets converts spec.Configurations to client.Configurations
// handling secret env vars by using secretKeyRef pointing to the K8s Secret created by SecretReference
func mapConfigurationsWithSecrets(specConfigs *spec.Configurations, secretReference string) *client.Configurations {
if specConfigs == nil || len(specConfigs.Env) == 0 {
return nil
}
configs := &client.Configurations{
Env: make([]client.EnvVar, len(specConfigs.Env)),
}
for i, env := range specConfigs.Env {
if env.GetIsSensitive() {
// Use secretKeyRef pointing to the K8s Secret
// Name = K8s Secret name (created by SecretReference)
// Key = key within the K8s Secret
configs.Env[i] = client.EnvVar{
Key: env.Key,
ValueFrom: &client.EnvVarValueFrom{
SecretKeyRef: &client.SecretKeyRef{
Name: secretReference, // K8s Secret name (e.g., "component-secrets")
Key: env.Key, // Key within the secret
},
},
}
} else {
configs.Env[i] = client.EnvVar{Key: env.Key, Value: env.GetValue()}
}
}
return configs
}
// mapRepository converts spec.RepositoryConfig to client.RepositoryConfig
func mapRepository(specRepo *spec.RepositoryConfig) *client.RepositoryConfig {
if specRepo == nil {
return nil
}
repo := &client.RepositoryConfig{
URL: specRepo.Url,
Branch: specRepo.Branch,
AppPath: specRepo.AppPath,
}
if specRepo.SecretRef.Get() != nil {
repo.SecretRef = *specRepo.SecretRef.Get()
}
return repo
}
// mapInputInterface converts spec.InputInterface to client.InputInterfaceConfig
func mapInputInterface(specInterface *spec.InputInterface) *client.InputInterfaceConfig {
if specInterface == nil {
return nil
}
config := &client.InputInterfaceConfig{
Type: specInterface.Type,
}
if specInterface.Port != nil {
config.Port = *specInterface.Port
}
if specInterface.BasePath != nil {
config.BasePath = *specInterface.BasePath
}
if specInterface.Schema != nil {
config.SchemaPath = specInterface.Schema.Path
}
return config
}
// buildCreateTraitRequests collects all traits needed during agent creation into a single
// list so they can be attached in one GET-UPDATE cycle, avoiding resource version conflicts.
func (s *agentManagerService) buildCreateTraitRequests(ctx context.Context, orgName, projectName string, req *spec.CreateAgentRequest) ([]client.TraitRequest, error) {
var traits []client.TraitRequest
// Determine instrumentation trait
autoInstrumentation := req.Configurations == nil || req.Configurations.EnableAutoInstrumentation == nil || *req.Configurations.EnableAutoInstrumentation
isAPIAgent := req.AgentType.Type == string(utils.AgentTypeAPI)
isPythonBuildpack := req.Build != nil && req.Build.BuildpackBuild != nil && req.Build.BuildpackBuild.Buildpack.Language == string(utils.LanguagePython)
isDocker := req.Build != nil && req.Build.DockerBuild != nil
// Only generate API key when an instrumentation trait is needed
needsOTEL := isAPIAgent && autoInstrumentation && isPythonBuildpack
needsEnvInjection := isAPIAgent && (isDocker || (!autoInstrumentation && isPythonBuildpack))
if needsOTEL || needsEnvInjection {
apiKey, err := s.generateAgentAPIKey(ctx, orgName, projectName, req.Name)
if err != nil {
return nil, fmt.Errorf("failed to generate agent API key: %w", err)
}
if needsOTEL {
traits = append(traits, client.TraitRequest{TraitKind: client.TraitKindTrait, TraitType: client.TraitOTELInstrumentation, Opts: []client.TraitOption{client.WithAgentApiKey(apiKey)}})
} else {
traits = append(traits, client.TraitRequest{TraitKind: client.TraitKindTrait, TraitType: client.TraitEnvInjection, Opts: []client.TraitOption{client.WithAgentApiKey(apiKey)}})
}
}
// API configuration trait (only for chat and custom API agents)
if isAPIAgent {
var traitOpts []client.TraitOption
if req.InputInterface != nil && req.InputInterface.HasPort() && req.InputInterface.GetPort() > 0 {
traitOpts = append(traitOpts, client.WithUpstreamPort(req.InputInterface.GetPort()))
} else {
traitOpts = append(traitOpts, client.WithUpstreamPort(config.GetConfig().DefaultChatAPI.DefaultHTTPPort))
}
if req.InputInterface != nil && req.InputInterface.HasBasePath() {
traitOpts = append(traitOpts, client.WithUpstreamBasePath(req.InputInterface.GetBasePath()))
} else {
traitOpts = append(traitOpts, client.WithUpstreamBasePath(config.GetConfig().DefaultChatAPI.DefaultBasePath))
}
traits = append(traits, client.TraitRequest{TraitKind: client.TraitKindTrait, TraitType: client.TraitAPIManagement, Opts: traitOpts})
}
return traits, nil
}
// attachOTELInstrumentationTrait attaches OTEL instrumentation trait to the agent
// The trait handles injection of OTEL configuration including the agent API key
func (s *agentManagerService) attachOTELInstrumentationTrait(ctx context.Context, orgName, projectName, agentName string) error {
// Generate agent API key for the trait parameters
apiKey, err := s.generateAgentAPIKey(ctx, orgName, projectName, agentName)
if err != nil {
return fmt.Errorf("failed to generate agent API key: %w", err)
}
if err := s.ocClient.AttachTraits(ctx, orgName, projectName, agentName, []client.TraitRequest{
{TraitKind: client.TraitKindTrait, TraitType: client.TraitOTELInstrumentation, Opts: []client.TraitOption{client.WithAgentApiKey(apiKey)}},
}); err != nil {
return fmt.Errorf("error attaching OTEL instrumentation trait: %w", err)
}
s.logger.Info("Enabled instrumentation for buildpack agent", "agentName", agentName)
return nil
}
// detachOTELInstrumentationTrait removes the OTEL instrumentation trait from the agent
func (s *agentManagerService) detachOTELInstrumentationTrait(ctx context.Context, orgName, projectName, agentName string) error {
if err := s.ocClient.DetachTrait(ctx, orgName, projectName, agentName, client.TraitOTELInstrumentation); err != nil {
return fmt.Errorf("error detaching OTEL instrumentation trait: %w", err)
}
s.logger.Info("Disabled instrumentation for buildpack agent", "agentName", agentName)
return nil
}
// attachEnvInjectionTrait attaches the env injection trait to inject AMP_OTEL_ENDPOINT
// and AMP_AGENT_API_KEY environment variables. Used for Docker builds and buildpack
// builds when auto-instrumentation is disabled.
func (s *agentManagerService) attachEnvInjectionTrait(ctx context.Context, orgName, projectName, agentName string) error {
// Generate agent API key for the trait parameters
apiKey, err := s.generateAgentAPIKey(ctx, orgName, projectName, agentName)
if err != nil {
return fmt.Errorf("failed to generate agent API key: %w", err)
}
if err := s.ocClient.AttachTraits(ctx, orgName, projectName, agentName, []client.TraitRequest{
{TraitKind: client.TraitKindTrait, TraitType: client.TraitEnvInjection, Opts: []client.TraitOption{client.WithAgentApiKey(apiKey)}},
}); err != nil {
return fmt.Errorf("error attaching env injection trait: %w", err)
}
s.logger.Info("Attached env injection trait", "agentName", agentName)
return nil
}
// detachEnvInjectionTrait removes the env injection trait from the agent
func (s *agentManagerService) detachEnvInjectionTrait(ctx context.Context, orgName, projectName, agentName string) error {
if err := s.ocClient.DetachTrait(ctx, orgName, projectName, agentName, client.TraitEnvInjection); err != nil {
return fmt.Errorf("error detaching env injection trait: %w", err)
}
s.logger.Info("Detached env injection trait", "agentName", agentName)
return nil
}
// persistInstrumentationConfig saves the instrumentation config to the database
func (s *agentManagerService) persistInstrumentationConfig(ctx context.Context, orgName, projectName, agentName string, enableAutoInstrumentation bool) {
// Get the first/lowest environment
pipeline, err := s.ocClient.GetProjectDeploymentPipeline(ctx, orgName, projectName)
if err != nil {
s.logger.Warn("Failed to get deployment pipeline for config persistence", "agentName", agentName, "error", err)
return
}
lowestEnv := findLowestEnvironment(pipeline.PromotionPaths)
if lowestEnv == "" {
s.logger.Warn("No environment found for config persistence", "agentName", agentName)
return
}
targetEnv, err := s.ocClient.GetEnvironment(ctx, orgName, lowestEnv)
if err != nil {
s.logger.Warn("Failed to get environment details for config persistence", "agentName", agentName, "environment", lowestEnv, "error", err)
return
}
agentConfig := &models.AgentConfig{
OrgName: orgName,
ProjectName: projectName,
AgentName: agentName,
EnvironmentName: targetEnv.Name,
EnableAutoInstrumentation: enableAutoInstrumentation,
}
if err := s.agentConfigRepo.Upsert(agentConfig); err != nil {
s.logger.Warn("Failed to persist instrumentation config to database", "agentName", agentName, "error", err)
} else {
s.logger.Debug("Persisted instrumentation config to database", "agentName", agentName, "environment", lowestEnv, "enableAutoInstrumentation", enableAutoInstrumentation)
}
}
// generateAgentAPIKey generates an agent API key (JWT token) for the agent
// This is a common utility used by both buildpack and docker agent instrumentation
func (s *agentManagerService) generateAgentAPIKey(ctx context.Context, orgName, projectName, agentName string) (string, error) {
// Get the deployment pipeline to find the first environment
pipeline, err := s.ocClient.GetProjectDeploymentPipeline(ctx, orgName, projectName)
if err != nil {
s.logger.Error("Failed to get deployment pipeline for token generation", "projectName", projectName, "error", err)
return "", translatePipelineError(err)
}
firstEnvName := findLowestEnvironment(pipeline.PromotionPaths)
// Extract OrgId from the caller's JWT claims
callerClaims := jwtassertion.GetTokenClaims(ctx)
if callerClaims == nil || callerClaims.OuId == "" {
s.logger.Error("GenerateToken: missing organization identity in caller token")
return "", utils.ErrForbidden
}
// Generate agent API key using token manager service with 1 year expiry
tokenReq := GenerateTokenRequest{
OrgName: orgName,
ProjectName: projectName,
AgentName: agentName,
Environment: firstEnvName,
ExpiresIn: "8760h", // 1 year (365 days * 24 hours)
OrgId: callerClaims.OuId,
}
tokenResp, err := s.tokenManagerService.GenerateToken(ctx, tokenReq)
if err != nil {
s.logger.Error("Failed to generate agent API key", "agentName", agentName, "error", err)
return "", fmt.Errorf("failed to generate agent API key: %w", err)
}
s.logger.Debug("Generated agent API key", "agentName", agentName)
return tokenResp.Token, nil
}
// generateTracingEnvVars generates tracing-related environment variables (OTEL endpoint and
// agent API key) for the named agent. Returns the env vars without persisting them.
func (s *agentManagerService) generateTracingEnvVars(ctx context.Context, orgName, projectName, agentName string) ([]client.EnvVar, error) {
s.logger.Debug("Generating tracing environment variables", "agentName", agentName)
// Generate agent API key
apiKey, err := s.generateAgentAPIKey(ctx, orgName, projectName, agentName)
if err != nil {
return nil, err
}
// Get OTEL exporter endpoint from config
cfg := config.GetConfig()
otelEndpoint := cfg.OTEL.ExporterEndpoint
// Prepare tracing environment variables
tracingEnvVars := []client.EnvVar{
{
Key: client.EnvVarOTELEndpoint,
Value: otelEndpoint,
},
{
Key: client.EnvVarAgentAPIKey,
Value: apiKey,
},
}
return tracingEnvVars, nil
}
// injectTracingEnvVarsByName injects tracing-related environment variables (OTEL endpoint and
// agent API key) for the named agent into the Component CR. This is used during agent creation
// for docker and Python buildpack agents (the latter when auto-instrumentation is disabled).
func (s *agentManagerService) injectTracingEnvVarsByName(ctx context.Context, orgName, projectName, agentName string) error {
s.logger.Debug("Injecting tracing environment variables", "agentName", agentName)
tracingEnvVars, err := s.generateTracingEnvVars(ctx, orgName, projectName, agentName)
if err != nil {
return err
}
// Update component configurations with tracing environment variables (for persistence)
if err := s.updateComponentEnvVars(ctx, orgName, projectName, agentName, tracingEnvVars); err != nil {
s.logger.Error("Failed to update component with tracing env vars", "agentName", agentName, "error", err)
return fmt.Errorf("failed to update component env vars: %w", err)
}
s.logger.Info("Injected tracing environment variables",
"agentName", agentName,
"envVarCount", len(tracingEnvVars),
)
return nil
}
// updateComponentEnvVars updates the component's workflow parameters with new environment variables
func (s *agentManagerService) updateComponentEnvVars(ctx context.Context, orgName, projectName, componentName string, newEnvVars []client.EnvVar) error {
s.logger.Debug("Updating component environment variables", "componentName", componentName, "newEnvCount", len(newEnvVars))
if err := s.ocClient.UpdateComponentEnvVars(ctx, orgName, projectName, componentName, newEnvVars); err != nil {
s.logger.Error("Failed to update component environment variables", "componentName", componentName, "error", err)
return fmt.Errorf("failed to update component environment variables: %w", err)
}
s.logger.Info("Successfully updated component environment variables",
"componentName", componentName,
"envVarCount", len(newEnvVars),
)
return nil
}
func (s *agentManagerService) GetAgent(ctx context.Context, orgName string, projectName string, agentName string) (*models.AgentResponse, error) {
s.logger.Info("Getting agent", "agentName", agentName, "orgName", orgName, "projectName", projectName)
// Validate organization exists
_, err := s.ocClient.GetOrganization(ctx, orgName)
if err != nil {
s.logger.Error("Failed to find organization", "orgName", orgName, "error", err)
return nil, translateOrgError(err)
}
agent, err := s.ocClient.GetComponent(ctx, orgName, projectName, agentName)
if err != nil {
s.logger.Error("Failed to fetch agent from OpenChoreo", "agentName", agentName, "orgName", orgName, "projectName", projectName, "error", err)
return nil, translateAgentError(err)
}
// Populate enableAutoInstrumentation from database
// Get the first/lowest environment to read the config
pipeline, pipelineErr := s.ocClient.GetProjectDeploymentPipeline(ctx, orgName, projectName)
if pipelineErr == nil && len(pipeline.PromotionPaths) > 0 {
lowestEnv := findLowestEnvironment(pipeline.PromotionPaths)
if lowestEnv != "" {
agentConfig, configErr := s.agentConfigRepo.Get(orgName, projectName, agentName, lowestEnv)
if errors.Is(configErr, repositories.ErrAgentConfigNotFound) {
// No config in DB - default to true for display purposes
defaultEnabled := true
agent.Configurations = &models.Configurations{
EnableAutoInstrumentation: &defaultEnabled,
}
s.logger.Debug("No agent config in database, defaulting to enabled", "agentName", agentName)
} else if configErr != nil {
s.logger.Warn("Failed to read agent config from database", "agentName", agentName, "environment", lowestEnv, "error", configErr)
} else {
agent.Configurations = &models.Configurations{
EnableAutoInstrumentation: &agentConfig.EnableAutoInstrumentation,
}
s.logger.Debug("Populated enableAutoInstrumentation from database", "agentName", agentName, "environment", lowestEnv, "enableAutoInstrumentation", agentConfig.EnableAutoInstrumentation)
}
}
}
s.logger.Info("Fetched agent successfully from oc", "agentName", agent.Name, "orgName", orgName, "projectName", projectName, "provisioningType", agent.Provisioning.Type)
return agent, nil
}
func (s *agentManagerService) ListAgents(ctx context.Context, orgName string, projName string, limit int32, offset int32) ([]*models.AgentResponse, int32, error) {
s.logger.Info("Listing agents", "orgName", orgName, "projectName", projName, "limit", limit, "offset", offset)
// Validate organization exists
_, err := s.ocClient.GetOrganization(ctx, orgName)
if err != nil {
s.logger.Error("Failed to find organization", "orgName", orgName, "error", err)
return nil, 0, translateOrgError(err)
}
// Fetch all agent components
agents, err := s.ocClient.ListComponents(ctx, orgName, projName)
if err != nil {
s.logger.Error("Failed to list agents from repository", "orgName", orgName, "projectName", projName, "error", err)
return nil, 0, fmt.Errorf("failed to list agents: %w", err)
}
// Calculate total count
total := int32(len(agents))
// Apply pagination
var paginatedAgents []*models.AgentResponse
if offset >= total {
// If offset is beyond available data, return empty slice
paginatedAgents = []*models.AgentResponse{}
} else {
endIndex := offset + limit
if endIndex > total {
endIndex = total
}
paginatedAgents = agents[offset:endIndex]
}
s.logger.Info("Listed agents successfully", "orgName", orgName, "projName", projName, "totalAgents", total, "returnedAgents", len(paginatedAgents))
return paginatedAgents, total, nil
}
func (s *agentManagerService) CreateAgent(ctx context.Context, orgName string, projectName string, req *spec.CreateAgentRequest) error {
s.logger.Info("Creating agent", "agentName", req.Name, "orgName", orgName, "projectName", projectName, "provisioningType", req.Provisioning.Type)
// Validate organization exists
_, err := s.ocClient.GetOrganization(ctx, orgName)
if err != nil {
s.logger.Error("Failed to find organization", "orgName", orgName, "error", err)
return translateOrgError(err)
}
// Validate git secret exists if specified
if req.Provisioning.Repository != nil && req.Provisioning.Repository.SecretRef.Get() != nil {
if err := s.validateGitSecretExists(ctx, orgName, req.Provisioning.Repository.GetSecretRef()); err != nil {
return err
}
}
// Get the first/lowest environment for secret path
pipeline, err := s.ocClient.GetProjectDeploymentPipeline(ctx, orgName, projectName)
if err != nil {
s.logger.Error("Failed to get deployment pipeline", "projectName", projectName, "error", err)
return translatePipelineError(err)
}
firstEnv := findLowestEnvironment(pipeline.PromotionPaths)
if firstEnv == "" {
s.logger.Error("No environment found in deployment pipeline", "projectName", projectName)
return fmt.Errorf("no environment found in deployment pipeline")
}
// Build secret location for OpenBao KV path
secretLocation := secretmanagersvc.SecretLocation{
OrgName: orgName,
ProjectName: projectName,
EnvironmentName: firstEnv,
EntityName: req.Name,
}
// Check if there are secret env vars that need to be handled
hasSecrets := false
if req.Configurations != nil && len(req.Configurations.Env) > 0 {
for _, env := range req.Configurations.Env {
if env.GetIsSensitive() {
hasSecrets = true
break
}
}
}
// Create SecretReference BEFORE Component so ReleaseBinding can find it
secretReference := ""
if hasSecrets {
secretReference, err = s.saveSecretsAndCreateReference(ctx, secretLocation, req.Configurations.Env)
if err != nil {
s.logger.Error("Failed to save secrets and create SecretReference for agent", "agentName", req.Name, "error", err)
s.cleanupSecretsOnRollback(ctx, secretLocation)
return err
}
}
// Create component request
createAgentReq := s.toCreateAgentRequestWithSecrets(req, secretReference)
if err := s.ocClient.CreateComponent(ctx, orgName, projectName, createAgentReq); err != nil {
s.logger.Error("Failed to create agent component", "agentName", req.Name, "error", err)
// Rollback secrets if component creation fails
if hasSecrets {
s.cleanupSecretsOnRollback(ctx, secretLocation)
}
return err
}
// Create LLM configurations (applies to both internal and external agents)
if len(req.ModelConfig) > 0 {
if err := s.createAgentLLMConfigs(ctx, orgName, projectName, req); err != nil {
s.logger.Error("Failed to create LLM configurations for agent", "agentName", req.Name, "error", err)
if hasSecrets {
s.cleanupSecretsOnRollback(ctx, secretLocation)
}
if errDeletion := s.ocClient.DeleteComponent(ctx, orgName, projectName, req.Name); errDeletion != nil {
s.logger.Error("Failed to rollback agent after LLM config failure", "agentName", req.Name, "error", errDeletion)
}
return err
}
}
// For internal agents, enable instrumentation (if enabled) and trigger initial build
if req.Provisioning.Type == string(utils.InternalAgent) {
s.logger.Debug("Component created successfully", "agentName", req.Name)
// Build all traits to attach in a single GET-UPDATE cycle to avoid resource version conflicts
traitRequests, err := s.buildCreateTraitRequests(ctx, orgName, projectName, req)
if err != nil {
s.logger.Error("Failed to build trait requests", "agentName", req.Name, "error", err)
if hasSecrets {
s.cleanupSecretsOnRollback(ctx, secretLocation)
}
if errDeletion := s.ocClient.DeleteComponent(ctx, orgName, projectName, req.Name); errDeletion != nil {
s.logger.Error("Failed to rollback agent creation after trait build failure", "agentName", req.Name, "error", errDeletion)
}
return err
}
if len(traitRequests) > 0 {
if err := s.ocClient.AttachTraits(ctx, orgName, projectName, req.Name, traitRequests); err != nil {
s.logger.Error("Failed to attach traits", "agentName", req.Name, "error", err)
if hasSecrets {
s.cleanupSecretsOnRollback(ctx, secretLocation)
}
if errDeletion := s.ocClient.DeleteComponent(ctx, orgName, projectName, req.Name); errDeletion != nil {
s.logger.Error("Failed to rollback agent creation after trait attachment failure", "agentName", req.Name, "error", errDeletion)
}
return err
}
s.logger.Info("Attached traits", "agentName", req.Name, "count", len(traitRequests))
}
// Trigger initial build (non-fatal - build can be triggered manually later)
if err := s.triggerInitialBuild(ctx, orgName, projectName, req); err != nil {
s.logger.Warn("Failed to trigger initial build for agent, build can be triggered manually", "agentName", req.Name, "error", err)
} else {
s.logger.Debug("Triggered initial build for agent", "agentName", req.Name)
}
// Persist initial instrumentation config to database
enableAutoInstrumentation := true // Default
if req.Configurations != nil && req.Configurations.EnableAutoInstrumentation != nil {
enableAutoInstrumentation = *req.Configurations.EnableAutoInstrumentation
}
s.persistInstrumentationConfig(ctx, orgName, projectName, req.Name, enableAutoInstrumentation)
}
s.logger.Info("Agent created successfully", "agentName", req.Name, "orgName", orgName, "projectName", projectName, "provisioningType", req.Provisioning.Type)
return nil
}
func (s *agentManagerService) triggerInitialBuild(ctx context.Context, orgName, projectName string, req *spec.CreateAgentRequest) error {
// Get the latest commit from the repository
commitId := ""
if req.Provisioning.Repository != nil {
repoURL := req.Provisioning.Repository.Url
branch := req.Provisioning.Repository.Branch
owner, repo := utils.ParseGitHubURL(repoURL)
if owner != "" && repo != "" {
latestCommit, err := s.gitRepositoryService.GetLatestCommit(ctx, owner, repo, branch)
if err != nil {
s.logger.Warn("Failed to get latest commit, will use empty commit", "repoURL", repoURL, "branch", branch, "error", err)
} else {
commitId = latestCommit
s.logger.Debug("Got latest commit for build", "commitId", commitId, "branch", branch)
}
}
}
// Trigger build in OpenChoreo with the latest commit
build, err := s.ocClient.TriggerBuild(ctx, orgName, projectName, req.Name, commitId)
if err != nil {
return fmt.Errorf("failed to trigger initial build: agentName %s, error: %w", req.Name, err)
}
s.logger.Info("Agent component created and build triggered successfully", "agentName", req.Name, "orgName", orgName, "projectName", projectName, "buildName", build.Name, "commitId", commitId)
return nil
}
func (s *agentManagerService) createAgentLLMConfigs(
ctx context.Context, orgName, projectName string, req *spec.CreateAgentRequest,
) error {
for i, mc := range req.ModelConfig {
configName := fmt.Sprintf("%s-llm-config", req.Name)
if len(req.ModelConfig) > 1 {
configName = fmt.Sprintf("%s-llm-config-%d", req.Name, i+1)
}
createReq := models.CreateAgentModelConfigRequest{
Name: configName,
Type: "llm",
EnvMappings: convertEnvMappings(mc.EnvMappings),
EnvironmentVariables: convertEnvVars(mc.EnvironmentVariables),
}
if _, err := s.agentConfigurationService.Create(ctx, orgName, projectName, req.Name, createReq, "system"); err != nil {
return fmt.Errorf("failed to create LLM configuration %d: %w", i+1, err)
}
}
return nil
}
func convertEnvMappings(specMappings map[string]spec.EnvModelConfigRequest) map[string]models.EnvModelConfigRequest {
result := make(map[string]models.EnvModelConfigRequest, len(specMappings))
for env, m := range specMappings {
policies := make([]models.LLMPolicy, 0, len(m.Configuration.Policies))
for _, p := range m.Configuration.Policies {
paths := make([]models.LLMPolicyPath, 0, len(p.Paths))
for _, pp := range p.Paths {
paths = append(paths, models.LLMPolicyPath{
Path: pp.Path,
Methods: pp.Methods,
Params: pp.Params,
})
}
policies = append(policies, models.LLMPolicy{
Name: p.Name,
Version: p.Version,
Paths: paths,
})
}
result[env] = models.EnvModelConfigRequest{
ProviderName: m.ProviderName,
Configuration: models.EnvProviderConfiguration{Policies: policies},
}
}
return result
}
func convertEnvVars(specVars []spec.EnvironmentVariableConfig) []models.EnvironmentVariableConfig {
result := make([]models.EnvironmentVariableConfig, 0, len(specVars))
for _, v := range specVars {
result = append(result, models.EnvironmentVariableConfig{Name: v.Name, Key: v.Key})
}
return result
}
// toCreateAgentRequestWithSecrets creates a component request, handling secrets by using secretKeyRef
func (s *agentManagerService) toCreateAgentRequestWithSecrets(req *spec.CreateAgentRequest, secretReferences string) client.CreateComponentRequest {
result := client.CreateComponentRequest{
Name: req.Name,
DisplayName: req.DisplayName,
Description: utils.StrPointerAsStr(req.Description, ""),
ProvisioningType: client.ProvisioningType(req.Provisioning.Type),
AgentType: client.AgentTypeConfig{
Type: req.AgentType.Type,
},
Repository: mapRepository(req.Provisioning.Repository),
Build: mapBuildConfig(req.Build),
InputInterface: mapInputInterface(req.InputInterface),
}
result.Configurations = mapConfigurationsWithSecrets(req.Configurations, secretReferences)
if req.Provisioning.Type == string(utils.InternalAgent) {
result.AgentType.SubType = utils.StrPointerAsStr(req.AgentType.SubType, "")
}
return result
}
// saveSecretsAndCreateReference handles storing secrets in OpenBao and creating SecretReference CR
func (s *agentManagerService) saveSecretsAndCreateReference(
ctx context.Context,
location secretmanagersvc.SecretLocation,
envVars []spec.EnvironmentVariable,
) (string, error) {
if s.secretMgmtClient == nil {
return "", fmt.Errorf("secret management is not initialized but secret env vars were provided")
}
// Collect secret data
secretData := make(map[string]string)
for _, env := range envVars {
if env.GetIsSensitive() {
secretData[env.Key] = env.GetValue()
}
}
if len(secretData) == 0 {
return "", nil
}
// Store secrets in KV via secretmanagersvc client
// SecretReference creation is handled internally by the client when ocClient is configured
kvPath, err := location.KVPath()
if err != nil {
return "", fmt.Errorf("invalid secret location: %w", err)
}
s.logger.Debug("Storing secrets in KV", "kvPath", kvPath, "secretRefName", location.SecretRefName(), "secretCount", len(secretData))
secretRef, createErr := s.secretMgmtClient.CreateSecret(ctx, location, secretData)
if createErr != nil {
if errors.Is(createErr, secretmanagersvc.ErrNotManaged) {
return "", fmt.Errorf("secret path %q is already owned by another system and cannot be overwritten; manual cleanup may be required: %w", kvPath, utils.ErrSecretPathConflict)
}
return "", fmt.Errorf("failed to store secrets in KV: %w", createErr)
}
s.logger.Info("Secrets stored and SecretReference created", "kvPath", kvPath, "secretCount", len(secretData))
return secretRef, nil
}
// cleanupSecretsOnRollback removes secrets from KV and deletes SecretReference CR during rollback.
// This is a best-effort cleanup - errors are logged but not returned since we're already handling a failure.
func (s *agentManagerService) cleanupSecretsOnRollback(ctx context.Context, location secretmanagersvc.SecretLocation) {
// Delete secrets from KV and SecretReference
if s.secretMgmtClient != nil {
kvPath, _ := location.KVPath()
if err := s.secretMgmtClient.DeleteSecret(ctx, location, location.SecretRefName()); err != nil {
s.logger.Warn("Failed to cleanup secrets during rollback", "kvPath", kvPath, "error", err)
} else {
s.logger.Debug("Cleaned up secrets during rollback", "kvPath", kvPath)
}
}
}
func (s *agentManagerService) UpdateAgentBasicInfo(ctx context.Context, orgName string, projectName string, agentName string, req *spec.UpdateAgentBasicInfoRequest) (*models.AgentResponse, error) {
s.logger.Info("Updating agent basic info", "agentName", agentName, "orgName", orgName, "projectName", projectName)
// Validate organization exists
_, err := s.ocClient.GetOrganization(ctx, orgName)
if err != nil {
s.logger.Error("Failed to find organization", "orgName", orgName, "error", err)
return nil, translateOrgError(err)
}
// Validate project exists
_, err = s.ocClient.GetProject(ctx, orgName, projectName)
if err != nil {
s.logger.Error("Failed to find project", "projectName", projectName, "org", orgName, "error", err)
return nil, translateProjectError(err)
}
// Fetch existing agent to validate it exists
_, err = s.ocClient.GetComponent(ctx, orgName, projectName, agentName)
if err != nil {
s.logger.Error("Failed to fetch existing agent", "agentName", agentName, "orgName", orgName, "projectName", projectName, "error", err)
return nil, translateAgentError(err)
}
// Update agent basic info in OpenChoreo
updateReq := client.UpdateComponentBasicInfoRequest{
DisplayName: req.DisplayName,
Description: req.Description,
}
if err := s.ocClient.UpdateComponentBasicInfo(ctx, orgName, projectName, agentName, updateReq); err != nil {
s.logger.Error("Failed to update agent meta data in OpenChoreo", "agentName", agentName, "orgName", orgName, "projectName", projectName, "error", err)
return nil, fmt.Errorf("failed to update agent basic info: %w", err)
}
// Fetch agent to return current state
updatedAgent, err := s.ocClient.GetComponent(ctx, orgName, projectName, agentName)
if err != nil {
s.logger.Error("Failed to fetch agent", "agentName", agentName, "orgName", orgName, "projectName", projectName, "error", err)
return nil, translateAgentError(err)
}
s.logger.Info("Agent basic info update called", "agentName", agentName, "orgName", orgName, "projectName", projectName)
return updatedAgent, nil
}
func (s *agentManagerService) UpdateAgentBuildParameters(ctx context.Context, orgName string, projectName string, agentName string, req *spec.UpdateAgentBuildParametersRequest) (*models.AgentResponse, error) {
s.logger.Info("Updating agent build parameters", "agentName", agentName, "orgName", orgName, "projectName", projectName)
// Validate organization exists
_, err := s.ocClient.GetOrganization(ctx, orgName)
if err != nil {
s.logger.Error("Failed to find organization", "orgName", orgName, "error", err)
return nil, translateOrgError(err)
}
// Validate project exists
_, err = s.ocClient.GetProject(ctx, orgName, projectName)
if err != nil {
s.logger.Error("Failed to find project", "projectName", projectName, "org", orgName, "error", err)
return nil, translateProjectError(err)
}
// Validate git secret exists if specified
if req.Provisioning.Repository != nil && req.Provisioning.Repository.SecretRef.Get() != nil {
if err := s.validateGitSecretExists(ctx, orgName, req.Provisioning.Repository.GetSecretRef()); err != nil {
return nil, err
}
}
// Fetch existing agent to validate immutable fields
existingAgent, err := s.ocClient.GetComponent(ctx, orgName, projectName, agentName)
if err != nil {
s.logger.Error("Failed to fetch existing agent", "agentName", agentName, "orgName", orgName, "projectName", projectName, "error", err)
return nil, translateAgentError(err)
}
// Check immutable fields - agentType cannot be changed if provided
if req.AgentType.Type != existingAgent.Type.Type {
s.logger.Error("Cannot change agent type", "existingType", existingAgent.Type.Type, "requestedType", req.AgentType.Type)
return nil, fmt.Errorf("%w: agent type cannot be changed", utils.ErrImmutableFieldChange)
}
// Check immutable fields - provisioning type cannot be changed if provided
if req.Provisioning.Type != existingAgent.Provisioning.Type {
s.logger.Error("Cannot change provisioning type", "existingType", existingAgent.Provisioning.Type, "requestedType", req.Provisioning.Type)
return nil, fmt.Errorf("%w: provisioning type cannot be changed", utils.ErrImmutableFieldChange)
}
// Update agent build parameters in OpenChoreo
updateReq := buildUpdateBuildParametersRequest(req)
if err := s.ocClient.UpdateComponentBuildParameters(ctx, orgName, projectName, agentName, updateReq); err != nil {
s.logger.Error("Failed to update agent build parameters in OpenChoreo", "agentName", agentName, "orgName", orgName, "projectName", projectName, "error", err)
return nil, fmt.Errorf("failed to update agent build parameters: %w", err)
}
// Fetch agent to return current state
updatedAgent, err := s.ocClient.GetComponent(ctx, orgName, projectName, agentName)
if err != nil {
s.logger.Error("Failed to fetch agent", "agentName", agentName, "orgName", orgName, "projectName", projectName, "error", err)