diff --git a/cmd/garm-cli/cmd/enterprise.go b/cmd/garm-cli/cmd/enterprise.go index fd2f45e1..eabfad26 100644 --- a/cmd/garm-cli/cmd/enterprise.go +++ b/cmd/garm-cli/cmd/enterprise.go @@ -183,6 +183,8 @@ func init() { enterpriseAddCmd.Flags().StringVar(&enterpriseCreds, "credentials", "", "Credentials name. See credentials list.") enterpriseAddCmd.Flags().StringVar(&poolBalancerType, "pool-balancer-type", string(params.PoolBalancerTypeRoundRobin), "The balancing strategy to use when creating runners in pools matching requested labels.") + enterpriseListCmd.Flags().BoolVarP(&long, "long", "l", false, "Include additional info.") + enterpriseAddCmd.MarkFlagRequired("credentials") //nolint enterpriseAddCmd.MarkFlagRequired("name") //nolint enterpriseUpdateCmd.Flags().StringVar(&enterpriseWebhookSecret, "webhook-secret", "", "The webhook secret for this enterprise") @@ -207,9 +209,16 @@ func formatEnterprises(enterprises []params.Enterprise) { } t := table.NewWriter() header := table.Row{"ID", "Name", "Endpoint", "Credentials name", "Pool Balancer Type", "Pool mgr running"} + if long { + header = append(header, "Created At", "Updated At") + } t.AppendHeader(header) for _, val := range enterprises { - t.AppendRow(table.Row{val.ID, val.Name, val.Endpoint.Name, val.Credentials.Name, val.GetBalancerType(), val.PoolManagerStatus.IsRunning}) + row := table.Row{val.ID, val.Name, val.Endpoint.Name, val.Credentials.Name, val.GetBalancerType(), val.PoolManagerStatus.IsRunning} + if long { + row = append(row, val.CreatedAt, val.UpdatedAt) + } + t.AppendRow(row) t.AppendSeparator() } fmt.Println(t.Render()) @@ -225,6 +234,8 @@ func formatOneEnterprise(enterprise params.Enterprise) { header := table.Row{"Field", "Value"} t.AppendHeader(header) t.AppendRow(table.Row{"ID", enterprise.ID}) + t.AppendRow(table.Row{"Created At", enterprise.CreatedAt}) + t.AppendRow(table.Row{"Updated At", enterprise.UpdatedAt}) t.AppendRow(table.Row{"Name", enterprise.Name}) t.AppendRow(table.Row{"Endpoint", enterprise.Endpoint.Name}) t.AppendRow(table.Row{"Pool balancer type", enterprise.GetBalancerType()}) diff --git a/cmd/garm-cli/cmd/github_credentials.go b/cmd/garm-cli/cmd/github_credentials.go index 8d75e33b..c4faec1a 100644 --- a/cmd/garm-cli/cmd/github_credentials.go +++ b/cmd/garm-cli/cmd/github_credentials.go @@ -223,6 +223,8 @@ func init() { githubCredentialsUpdateCmd.Flags().Int64Var(&credentialsAppID, "app-id", 0, "If the credential is an app, the app ID") githubCredentialsUpdateCmd.Flags().StringVar(&credentialsPrivateKeyPath, "private-key-path", "", "If the credential is an app, the path to the private key file") + githubCredentialsListCmd.Flags().BoolVarP(&long, "long", "l", false, "Include additional info.") + githubCredentialsUpdateCmd.MarkFlagsMutuallyExclusive("pat-oauth-token", "app-installation-id") githubCredentialsUpdateCmd.MarkFlagsMutuallyExclusive("pat-oauth-token", "app-id") githubCredentialsUpdateCmd.MarkFlagsMutuallyExclusive("pat-oauth-token", "private-key-path") @@ -349,9 +351,16 @@ func formatGithubCredentials(creds []params.GithubCredentials) { } t := table.NewWriter() header := table.Row{"ID", "Name", "Description", "Base URL", "API URL", "Upload URL", "Type"} + if long { + header = append(header, "Created At", "Updated At") + } t.AppendHeader(header) for _, val := range creds { - t.AppendRow(table.Row{val.ID, val.Name, val.Description, val.BaseURL, val.APIBaseURL, val.UploadBaseURL, val.AuthType}) + row := table.Row{val.ID, val.Name, val.Description, val.BaseURL, val.APIBaseURL, val.UploadBaseURL, val.AuthType} + if long { + row = append(row, val.CreatedAt, val.UpdatedAt) + } + t.AppendRow(row) t.AppendSeparator() } fmt.Println(t.Render()) @@ -367,6 +376,8 @@ func formatOneGithubCredential(cred params.GithubCredentials) { t.AppendHeader(header) t.AppendRow(table.Row{"ID", cred.ID}) + t.AppendRow(table.Row{"Created At", cred.CreatedAt}) + t.AppendRow(table.Row{"Updated At", cred.UpdatedAt}) t.AppendRow(table.Row{"Name", cred.Name}) t.AppendRow(table.Row{"Description", cred.Description}) t.AppendRow(table.Row{"Base URL", cred.BaseURL}) diff --git a/cmd/garm-cli/cmd/github_endpoints.go b/cmd/garm-cli/cmd/github_endpoints.go index c2f611e7..2be14f52 100644 --- a/cmd/garm-cli/cmd/github_endpoints.go +++ b/cmd/garm-cli/cmd/github_endpoints.go @@ -189,6 +189,8 @@ func init() { githubEndpointCreateCmd.Flags().StringVar(&endpointAPIBaseURL, "api-base-url", "", "API Base URL of the GitHub endpoint") githubEndpointCreateCmd.Flags().StringVar(&endpointCACertPath, "ca-cert-path", "", "CA Cert Path of the GitHub endpoint") + githubEndpointListCmd.Flags().BoolVarP(&long, "long", "l", false, "Include additional info.") + githubEndpointCreateCmd.MarkFlagRequired("name") githubEndpointCreateCmd.MarkFlagRequired("base-url") githubEndpointCreateCmd.MarkFlagRequired("api-base-url") @@ -257,9 +259,16 @@ func formatEndpoints(endpoints params.GithubEndpoints) { } t := table.NewWriter() header := table.Row{"Name", "Base URL", "Description"} + if long { + header = append(header, "Created At", "Updated At") + } t.AppendHeader(header) for _, val := range endpoints { - t.AppendRow([]interface{}{val.Name, val.BaseURL, val.Description}) + row := table.Row{val.Name, val.BaseURL, val.Description} + if long { + row = append(row, val.CreatedAt, val.UpdatedAt) + } + t.AppendRow(row) t.AppendSeparator() } fmt.Println(t.Render()) @@ -274,6 +283,9 @@ func formatOneEndpoint(endpoint params.GithubEndpoint) { header := table.Row{"Field", "Value"} t.AppendHeader(header) t.AppendRow([]interface{}{"Name", endpoint.Name}) + t.AppendRow([]interface{}{"Description", endpoint.Description}) + t.AppendRow([]interface{}{"Created At", endpoint.CreatedAt}) + t.AppendRow([]interface{}{"Updated At", endpoint.UpdatedAt}) t.AppendRow([]interface{}{"Base URL", endpoint.BaseURL}) t.AppendRow([]interface{}{"Upload URL", endpoint.UploadBaseURL}) t.AppendRow([]interface{}{"API Base URL", endpoint.APIBaseURL}) diff --git a/cmd/garm-cli/cmd/organization.go b/cmd/garm-cli/cmd/organization.go index 7b96e0fa..c7be1f19 100644 --- a/cmd/garm-cli/cmd/organization.go +++ b/cmd/garm-cli/cmd/organization.go @@ -311,6 +311,7 @@ func init() { orgAddCmd.Flags().BoolVar(&installOrgWebhook, "install-webhook", false, "Install the webhook as part of the add operation.") orgAddCmd.MarkFlagsMutuallyExclusive("webhook-secret", "random-webhook-secret") orgAddCmd.MarkFlagsOneRequired("webhook-secret", "random-webhook-secret") + orgListCmd.Flags().BoolVarP(&long, "long", "l", false, "Include additional info.") orgAddCmd.MarkFlagRequired("credentials") //nolint orgAddCmd.MarkFlagRequired("name") //nolint @@ -347,9 +348,16 @@ func formatOrganizations(orgs []params.Organization) { } t := table.NewWriter() header := table.Row{"ID", "Name", "Endpoint", "Credentials name", "Pool Balancer Type", "Pool mgr running"} + if long { + header = append(header, "Created At", "Updated At") + } t.AppendHeader(header) for _, val := range orgs { - t.AppendRow(table.Row{val.ID, val.Name, val.Endpoint.Name, val.CredentialsName, val.GetBalancerType(), val.PoolManagerStatus.IsRunning}) + row := table.Row{val.ID, val.Name, val.Endpoint.Name, val.CredentialsName, val.GetBalancerType(), val.PoolManagerStatus.IsRunning} + if long { + row = append(row, val.CreatedAt, val.UpdatedAt) + } + t.AppendRow(row) t.AppendSeparator() } fmt.Println(t.Render()) @@ -365,10 +373,14 @@ func formatOneOrganization(org params.Organization) { header := table.Row{"Field", "Value"} t.AppendHeader(header) t.AppendRow(table.Row{"ID", org.ID}) + t.AppendRow(table.Row{"Created At", org.CreatedAt}) + t.AppendRow(table.Row{"Updated At", org.UpdatedAt}) t.AppendRow(table.Row{"Name", org.Name}) t.AppendRow(table.Row{"Endpoint", org.Endpoint.Name}) t.AppendRow(table.Row{"Pool balancer type", org.GetBalancerType()}) t.AppendRow(table.Row{"Credentials", org.CredentialsName}) + t.AppendRow(table.Row{"Created at", org.CreatedAt}) + t.AppendRow(table.Row{"Updated at", org.UpdatedAt}) t.AppendRow(table.Row{"Pool manager running", org.PoolManagerStatus.IsRunning}) if !org.PoolManagerStatus.IsRunning { t.AppendRow(table.Row{"Failure reason", org.PoolManagerStatus.FailureReason}) diff --git a/cmd/garm-cli/cmd/pool.go b/cmd/garm-cli/cmd/pool.go index 407f9eb9..a4eee742 100644 --- a/cmd/garm-cli/cmd/pool.go +++ b/cmd/garm-cli/cmd/pool.go @@ -386,6 +386,7 @@ func init() { poolListCmd.Flags().StringVarP(&poolOrganization, "org", "o", "", "List all pools within this organization.") poolListCmd.Flags().StringVarP(&poolEnterprise, "enterprise", "e", "", "List all pools within this enterprise.") poolListCmd.Flags().BoolVarP(&poolAll, "all", "a", false, "List all pools, regardless of org or repo.") + poolListCmd.Flags().BoolVarP(&long, "long", "l", false, "Include additional info.") poolListCmd.MarkFlagsMutuallyExclusive("repo", "org", "all", "enterprise") poolUpdateCmd.Flags().StringVar(&poolImage, "image", "", "The provider-specific image name to use for runners in this pool.") @@ -472,7 +473,13 @@ func formatPools(pools []params.Pool) { return } t := table.NewWriter() - header := table.Row{"ID", "Image", "Flavor", "Tags", "Belongs to", "Level", "Enabled", "Runner Prefix", "Priority"} + t.SetColumnConfigs([]table.ColumnConfig{ + {Number: 2, WidthMax: 40}, + }) + header := table.Row{"ID", "Image", "Flavor", "Tags", "Belongs to", "Enabled"} + if long { + header = append(header, "Level", "Created At", "Updated at", "Runner Prefix", "Priority") + } t.AppendHeader(header) for _, pool := range pools { @@ -494,7 +501,11 @@ func formatPools(pools []params.Pool) { belongsTo = pool.EnterpriseName level = "enterprise" } - t.AppendRow(table.Row{pool.ID, pool.Image, pool.Flavor, strings.Join(tags, " "), belongsTo, level, pool.Enabled, pool.GetRunnerPrefix(), pool.Priority}) + row := table.Row{pool.ID, pool.Image, pool.Flavor, strings.Join(tags, " "), belongsTo, pool.Enabled} + if long { + row = append(row, level, pool.CreatedAt, pool.UpdatedAt, pool.GetRunnerPrefix(), pool.Priority) + } + t.AppendRow(row) t.AppendSeparator() } fmt.Println(t.Render()) @@ -532,6 +543,8 @@ func formatOnePool(pool params.Pool) { t.AppendHeader(header) t.AppendRow(table.Row{"ID", pool.ID}) + t.AppendRow(table.Row{"Created At", pool.CreatedAt}) + t.AppendRow(table.Row{"Updated At", pool.UpdatedAt}) t.AppendRow(table.Row{"Provider Name", pool.ProviderName}) t.AppendRow(table.Row{"Priority", pool.Priority}) t.AppendRow(table.Row{"Image", pool.Image}) diff --git a/cmd/garm-cli/cmd/repository.go b/cmd/garm-cli/cmd/repository.go index 9c02a021..1c453836 100644 --- a/cmd/garm-cli/cmd/repository.go +++ b/cmd/garm-cli/cmd/repository.go @@ -316,6 +316,8 @@ func init() { repoAddCmd.MarkFlagsMutuallyExclusive("webhook-secret", "random-webhook-secret") repoAddCmd.MarkFlagsOneRequired("webhook-secret", "random-webhook-secret") + repoListCmd.Flags().BoolVarP(&long, "long", "l", false, "Include additional info.") + repoAddCmd.MarkFlagRequired("credentials") //nolint repoAddCmd.MarkFlagRequired("owner") //nolint repoAddCmd.MarkFlagRequired("name") //nolint @@ -353,9 +355,16 @@ func formatRepositories(repos []params.Repository) { } t := table.NewWriter() header := table.Row{"ID", "Owner", "Name", "Endpoint", "Credentials name", "Pool Balancer Type", "Pool mgr running"} + if long { + header = append(header, "Created At", "Updated At") + } t.AppendHeader(header) for _, val := range repos { - t.AppendRow(table.Row{val.ID, val.Owner, val.Name, val.Endpoint.Name, val.CredentialsName, val.GetBalancerType(), val.PoolManagerStatus.IsRunning}) + row := table.Row{val.ID, val.Owner, val.Name, val.Endpoint.Name, val.CredentialsName, val.GetBalancerType(), val.PoolManagerStatus.IsRunning} + if long { + row = append(row, val.CreatedAt, val.UpdatedAt) + } + t.AppendRow(row) t.AppendSeparator() } fmt.Println(t.Render()) @@ -371,6 +380,8 @@ func formatOneRepository(repo params.Repository) { header := table.Row{"Field", "Value"} t.AppendHeader(header) t.AppendRow(table.Row{"ID", repo.ID}) + t.AppendRow(table.Row{"Created At", repo.CreatedAt}) + t.AppendRow(table.Row{"Updated At", repo.UpdatedAt}) t.AppendRow(table.Row{"Owner", repo.Owner}) t.AppendRow(table.Row{"Name", repo.Name}) t.AppendRow(table.Row{"Endpoint", repo.Endpoint.Name}) diff --git a/cmd/garm-cli/cmd/runner.go b/cmd/garm-cli/cmd/runner.go index aeb9bbf2..08b9a6db 100644 --- a/cmd/garm-cli/cmd/runner.go +++ b/cmd/garm-cli/cmd/runner.go @@ -206,7 +206,7 @@ func init() { runnerListCmd.Flags().StringVarP(&runnerOrganization, "org", "o", "", "List all runners from all pools within this organization.") runnerListCmd.Flags().StringVarP(&runnerEnterprise, "enterprise", "e", "", "List all runners from all pools within this enterprise.") runnerListCmd.Flags().BoolVarP(&runnerAll, "all", "a", false, "List all runners, regardless of org or repo.") - runnerListCmd.Flags().BoolVarP(&long, "long", "l", false, "Include information about tasks.") + runnerListCmd.Flags().BoolVarP(&long, "long", "l", false, "Include additional info.") runnerListCmd.MarkFlagsMutuallyExclusive("repo", "org", "enterprise", "all") runnerDeleteCmd.Flags().BoolVarP(&forceRemove, "force-remove-runner", "f", false, "Forcefully remove a runner. If set to true, GARM will ignore provider errors when removing the runner.") @@ -230,15 +230,18 @@ func formatInstances(param []params.Instance, detailed bool) { t := table.NewWriter() header := table.Row{"Nr", "Name", "Status", "Runner Status", "Pool ID"} if detailed { - header = append(header, "Job Name", "Started At", "Run ID", "Repository") + header = append(header, "Created At", "Updated At", "Job Name", "Started At", "Run ID", "Repository") } t.AppendHeader(header) for idx, inst := range param { row := table.Row{idx + 1, inst.Name, inst.Status, inst.RunnerStatus, inst.PoolID} - if detailed && inst.Job != nil { - repo := fmt.Sprintf("%s/%s", inst.Job.RepositoryOwner, inst.Job.RepositoryName) - row = append(row, inst.Job.Name, inst.Job.StartedAt, inst.Job.RunID, repo) + if detailed { + row = append(row, inst.CreatedAt, inst.UpdatedAt) + if inst.Job != nil { + repo := fmt.Sprintf("%s/%s", inst.Job.RepositoryOwner, inst.Job.RepositoryName) + row = append(row, inst.Job.Name, inst.Job.StartedAt, inst.Job.RunID, repo) + } } t.AppendRow(row) t.AppendSeparator() @@ -257,6 +260,8 @@ func formatSingleInstance(instance params.Instance) { t.AppendHeader(header) t.AppendRow(table.Row{"ID", instance.ID}, table.RowConfig{AutoMerge: false}) + t.AppendRow(table.Row{"Created At", instance.CreatedAt}) + t.AppendRow(table.Row{"Updated At", instance.UpdatedAt}) t.AppendRow(table.Row{"Provider ID", instance.ProviderID}, table.RowConfig{AutoMerge: false}) t.AppendRow(table.Row{"Name", instance.Name}, table.RowConfig{AutoMerge: false}) t.AppendRow(table.Row{"OS Type", instance.OSType}, table.RowConfig{AutoMerge: false}) diff --git a/database/sql/github.go b/database/sql/github.go index b0911222..22e357bd 100644 --- a/database/sql/github.go +++ b/database/sql/github.go @@ -55,6 +55,8 @@ func (s *sqlDatabase) sqlToCommonGithubCredentials(creds GithubCredentials) (par UploadBaseURL: creds.Endpoint.UploadBaseURL, CABundle: creds.Endpoint.CACertBundle, AuthType: creds.AuthType, + CreatedAt: creds.CreatedAt, + UpdatedAt: creds.UpdatedAt, Endpoint: ep, CredentialsPayload: data, } @@ -94,6 +96,8 @@ func (s *sqlDatabase) sqlToCommonGithubEndpoint(ep GithubEndpoint) (params.Githu BaseURL: ep.BaseURL, UploadBaseURL: ep.UploadBaseURL, CACertBundle: ep.CACertBundle, + CreatedAt: ep.CreatedAt, + UpdatedAt: ep.UpdatedAt, }, nil } diff --git a/database/sql/util.go b/database/sql/util.go index 063ebe0d..cc2bbcb9 100644 --- a/database/sql/util.go +++ b/database/sql/util.go @@ -65,6 +65,7 @@ func (s *sqlDatabase) sqlToParamsInstance(instance Instance) (params.Instance, e MetadataURL: instance.MetadataURL, StatusMessages: []params.StatusMessage{}, CreateAttempt: instance.CreateAttempt, + CreatedAt: instance.CreatedAt, UpdatedAt: instance.UpdatedAt, TokenFetched: instance.TokenFetched, JitConfiguration: jitConfig, @@ -127,6 +128,8 @@ func (s *sqlDatabase) sqlToCommonOrganization(org Organization, detailed bool) ( WebhookSecret: string(secret), PoolBalancerType: org.PoolBalancerType, Endpoint: endpoint, + CreatedAt: org.CreatedAt, + UpdatedAt: org.UpdatedAt, } if org.CredentialsID != nil { @@ -175,6 +178,8 @@ func (s *sqlDatabase) sqlToCommonEnterprise(enterprise Enterprise, detailed bool Pools: make([]params.Pool, len(enterprise.Pools)), WebhookSecret: string(secret), PoolBalancerType: enterprise.PoolBalancerType, + CreatedAt: enterprise.CreatedAt, + UpdatedAt: enterprise.UpdatedAt, Endpoint: endpoint, } @@ -224,6 +229,8 @@ func (s *sqlDatabase) sqlToCommonPool(pool Pool) (params.Pool, error) { ExtraSpecs: json.RawMessage(pool.ExtraSpecs), GitHubRunnerGroup: pool.GitHubRunnerGroup, Priority: pool.Priority, + CreatedAt: pool.CreatedAt, + UpdatedAt: pool.UpdatedAt, } if pool.RepoID != nil { @@ -285,6 +292,8 @@ func (s *sqlDatabase) sqlToCommonRepository(repo Repository, detailed bool) (par Pools: make([]params.Pool, len(repo.Pools)), WebhookSecret: string(secret), PoolBalancerType: repo.PoolBalancerType, + CreatedAt: repo.CreatedAt, + UpdatedAt: repo.UpdatedAt, Endpoint: endpoint, } diff --git a/params/params.go b/params/params.go index a93c2a3e..df822ce5 100644 --- a/params/params.go +++ b/params/params.go @@ -186,6 +186,9 @@ type Instance struct { // up. StatusMessages []StatusMessage `json:"status_messages,omitempty"` + // CreatedAt is the timestamp of the creation of this runner. + CreatedAt time.Time `json:"created_at,omitempty"` + // UpdatedAt is the timestamp of the last update to this runner. UpdatedAt time.Time `json:"updated_at,omitempty"` @@ -305,6 +308,8 @@ type Pool struct { EnterpriseID string `json:"enterprise_id,omitempty"` EnterpriseName string `json:"enterprise_name,omitempty"` RunnerBootstrapTimeout uint `json:"runner_bootstrap_timeout,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` // ExtraSpecs is an opaque raw json that gets sent to the provider // as part of the bootstrap params for instances. It can contain // any kind of data needed by providers. The contents of this field means @@ -396,6 +401,8 @@ type Repository struct { PoolManagerStatus PoolManagerStatus `json:"pool_manager_status,omitempty"` PoolBalancerType PoolBalancerType `json:"pool_balancing_type,omitempty"` Endpoint GithubEndpoint `json:"endpoint,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` // Do not serialize sensitive info. WebhookSecret string `json:"-"` } @@ -450,6 +457,8 @@ type Organization struct { PoolManagerStatus PoolManagerStatus `json:"pool_manager_status,omitempty"` PoolBalancerType PoolBalancerType `json:"pool_balancing_type,omitempty"` Endpoint GithubEndpoint `json:"endpoint,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` // Do not serialize sensitive info. WebhookSecret string `json:"-"` } @@ -499,6 +508,8 @@ type Enterprise struct { PoolManagerStatus PoolManagerStatus `json:"pool_manager_status,omitempty"` PoolBalancerType PoolBalancerType `json:"pool_balancing_type,omitempty"` Endpoint GithubEndpoint `json:"endpoint,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` // Do not serialize sensitive info. WebhookSecret string `json:"-"` } @@ -614,6 +625,8 @@ type GithubCredentials struct { Organizations []Organization `json:"organizations,omitempty"` Enterprises []Enterprise `json:"enterprises,omitempty"` Endpoint GithubEndpoint `json:"endpoint,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` // Do not serialize sensitive info. CredentialsPayload []byte `json:"-"` @@ -856,12 +869,14 @@ func (g GithubEntity) String() string { type GithubEndpoints []GithubEndpoint type GithubEndpoint struct { - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - APIBaseURL string `json:"api_base_url,omitempty"` - UploadBaseURL string `json:"upload_base_url,omitempty"` - BaseURL string `json:"base_url,omitempty"` - CACertBundle []byte `json:"ca_cert_bundle,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + APIBaseURL string `json:"api_base_url,omitempty"` + UploadBaseURL string `json:"upload_base_url,omitempty"` + BaseURL string `json:"base_url,omitempty"` + CACertBundle []byte `json:"ca_cert_bundle,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` Credentials []GithubCredentials `json:"credentials,omitempty"` }