feat: Add tag scopes to front and backend, initial commit

This commit is contained in:
Christoph Kluge 2024-08-01 18:59:24 +02:00
parent c80d3a6958
commit 017f9b2140
18 changed files with 361 additions and 98 deletions

View File

@ -113,6 +113,7 @@ type Tag {
id: ID! id: ID!
type: String! type: String!
name: String! name: String!
scope: String!
} }
type Resource { type Resource {
@ -235,7 +236,7 @@ type Query {
} }
type Mutation { type Mutation {
createTag(type: String!, name: String!): Tag! createTag(type: String!, name: String!, scope: String!): Tag!
deleteTag(id: ID!): ID! deleteTag(id: ID!): ID!
addTagsToJob(job: ID!, tagIds: [ID!]!): [Tag!]! addTagsToJob(job: ID!, tagIds: [ID!]!): [Tag!]!
removeTagsFromJob(job: ID!, tagIds: [ID!]!): [Tag!]! removeTagsFromJob(job: ID!, tagIds: [ID!]!): [Tag!]!

View File

@ -176,8 +176,9 @@ type ErrorResponse struct {
// ApiTag model // ApiTag model
type ApiTag struct { type ApiTag struct {
// Tag Type // Tag Type
Type string `json:"type" example:"Debug"` Type string `json:"type" example:"Debug"`
Name string `json:"name" example:"Testjob"` // Tag Name Name string `json:"name" example:"Testjob"` // Tag Name
Scope string `json:"scope" example:"global"` // Tag Scope for Frontend Display
} }
// ApiMeta model // ApiMeta model
@ -419,7 +420,7 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
StartTime: job.StartTime.Unix(), StartTime: job.StartTime.Unix(),
} }
res.Tags, err = api.JobRepository.GetTags(&job.ID) res.Tags, err = api.JobRepository.GetTags(r.Context(), &job.ID)
if err != nil { if err != nil {
handleError(err, http.StatusInternalServerError, rw) handleError(err, http.StatusInternalServerError, rw)
return return
@ -492,7 +493,7 @@ func (api *RestApi) getCompleteJobById(rw http.ResponseWriter, r *http.Request)
return return
} }
job.Tags, err = api.JobRepository.GetTags(&job.ID) job.Tags, err = api.JobRepository.GetTags(r.Context(), &job.ID)
if err != nil { if err != nil {
handleError(err, http.StatusInternalServerError, rw) handleError(err, http.StatusInternalServerError, rw)
return return
@ -578,7 +579,7 @@ func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) {
return return
} }
job.Tags, err = api.JobRepository.GetTags(&job.ID) job.Tags, err = api.JobRepository.GetTags(r.Context(), &job.ID)
if err != nil { if err != nil {
handleError(err, http.StatusInternalServerError, rw) handleError(err, http.StatusInternalServerError, rw)
return return
@ -711,7 +712,7 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
return return
} }
job.Tags, err = api.JobRepository.GetTags(&job.ID) job.Tags, err = api.JobRepository.GetTags(r.Context(), &job.ID)
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
return return
@ -724,16 +725,17 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
} }
for _, tag := range req { for _, tag := range req {
tagId, err := api.JobRepository.AddTagOrCreate(job.ID, tag.Type, tag.Name) tagId, err := api.JobRepository.AddTagOrCreate(r.Context(), job.ID, tag.Type, tag.Name, tag.Scope)
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
return return
} }
job.Tags = append(job.Tags, &schema.Tag{ job.Tags = append(job.Tags, &schema.Tag{
ID: tagId, ID: tagId,
Type: tag.Type, Type: tag.Type,
Name: tag.Name, Name: tag.Name,
Scope: tag.Scope,
}) })
} }
@ -801,7 +803,7 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
unlockOnce.Do(api.RepositoryMutex.Unlock) unlockOnce.Do(api.RepositoryMutex.Unlock)
for _, tag := range req.Tags { for _, tag := range req.Tags {
if _, err := api.JobRepository.AddTagOrCreate(id, tag.Type, tag.Name); err != nil { if _, err := api.JobRepository.AddTagOrCreate(r.Context(), id, tag.Type, tag.Name, tag.Scope); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
handleError(fmt.Errorf("adding tag to new job %d failed: %w", id, err), http.StatusInternalServerError, rw) handleError(fmt.Errorf("adding tag to new job %d failed: %w", id, err), http.StatusInternalServerError, rw)
return return

View File

@ -229,7 +229,7 @@ type ComplexityRoot struct {
Mutation struct { Mutation struct {
AddTagsToJob func(childComplexity int, job string, tagIds []string) int AddTagsToJob func(childComplexity int, job string, tagIds []string) int
CreateTag func(childComplexity int, typeArg string, name string) int CreateTag func(childComplexity int, typeArg string, name string, scope string) int
DeleteTag func(childComplexity int, id string) int DeleteTag func(childComplexity int, id string) int
RemoveTagsFromJob func(childComplexity int, job string, tagIds []string) int RemoveTagsFromJob func(childComplexity int, job string, tagIds []string) int
UpdateConfiguration func(childComplexity int, name string, value string) int UpdateConfiguration func(childComplexity int, name string, value string) int
@ -303,9 +303,10 @@ type ComplexityRoot struct {
} }
Tag struct { Tag struct {
ID func(childComplexity int) int ID func(childComplexity int) int
Name func(childComplexity int) int Name func(childComplexity int) int
Type func(childComplexity int) int Scope func(childComplexity int) int
Type func(childComplexity int) int
} }
TimeRangeOutput struct { TimeRangeOutput struct {
@ -355,7 +356,7 @@ type MetricValueResolver interface {
Name(ctx context.Context, obj *schema.MetricValue) (*string, error) Name(ctx context.Context, obj *schema.MetricValue) (*string, error)
} }
type MutationResolver interface { type MutationResolver interface {
CreateTag(ctx context.Context, typeArg string, name string) (*schema.Tag, error) CreateTag(ctx context.Context, typeArg string, name string, scope string) (*schema.Tag, error)
DeleteTag(ctx context.Context, id string) (string, error) DeleteTag(ctx context.Context, id string) (string, error)
AddTagsToJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) AddTagsToJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error)
RemoveTagsFromJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) RemoveTagsFromJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error)
@ -1183,7 +1184,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false return 0, false
} }
return e.complexity.Mutation.CreateTag(childComplexity, args["type"].(string), args["name"].(string)), true return e.complexity.Mutation.CreateTag(childComplexity, args["type"].(string), args["name"].(string), args["scope"].(string)), true
case "Mutation.deleteTag": case "Mutation.deleteTag":
if e.complexity.Mutation.DeleteTag == nil { if e.complexity.Mutation.DeleteTag == nil {
@ -1602,6 +1603,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Tag.Name(childComplexity), true return e.complexity.Tag.Name(childComplexity), true
case "Tag.scope":
if e.complexity.Tag.Scope == nil {
break
}
return e.complexity.Tag.Scope(childComplexity), true
case "Tag.type": case "Tag.type":
if e.complexity.Tag.Type == nil { if e.complexity.Tag.Type == nil {
break break
@ -1949,6 +1957,7 @@ type Tag {
id: ID! id: ID!
type: String! type: String!
name: String! name: String!
scope: String!
} }
type Resource { type Resource {
@ -2071,7 +2080,7 @@ type Query {
} }
type Mutation { type Mutation {
createTag(type: String!, name: String!): Tag! createTag(type: String!, name: String!, scope: String!): Tag!
deleteTag(id: ID!): ID! deleteTag(id: ID!): ID!
addTagsToJob(job: ID!, tagIds: [ID!]!): [Tag!]! addTagsToJob(job: ID!, tagIds: [ID!]!): [Tag!]!
removeTagsFromJob(job: ID!, tagIds: [ID!]!): [Tag!]! removeTagsFromJob(job: ID!, tagIds: [ID!]!): [Tag!]!
@ -2244,6 +2253,15 @@ func (ec *executionContext) field_Mutation_createTag_args(ctx context.Context, r
} }
} }
args["name"] = arg1 args["name"] = arg1
var arg2 string
if tmp, ok := rawArgs["scope"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("scope"))
arg2, err = ec.unmarshalNString2string(ctx, tmp)
if err != nil {
return nil, err
}
}
args["scope"] = arg2
return args, nil return args, nil
} }
@ -4622,6 +4640,8 @@ func (ec *executionContext) fieldContext_Job_tags(_ context.Context, field graph
return ec.fieldContext_Tag_type(ctx, field) return ec.fieldContext_Tag_type(ctx, field)
case "name": case "name":
return ec.fieldContext_Tag_name(ctx, field) return ec.fieldContext_Tag_name(ctx, field)
case "scope":
return ec.fieldContext_Tag_scope(ctx, field)
} }
return nil, fmt.Errorf("no field named %q was found under type Tag", field.Name) return nil, fmt.Errorf("no field named %q was found under type Tag", field.Name)
}, },
@ -7680,7 +7700,7 @@ func (ec *executionContext) _Mutation_createTag(ctx context.Context, field graph
}() }()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().CreateTag(rctx, fc.Args["type"].(string), fc.Args["name"].(string)) return ec.resolvers.Mutation().CreateTag(rctx, fc.Args["type"].(string), fc.Args["name"].(string), fc.Args["scope"].(string))
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@ -7711,6 +7731,8 @@ func (ec *executionContext) fieldContext_Mutation_createTag(ctx context.Context,
return ec.fieldContext_Tag_type(ctx, field) return ec.fieldContext_Tag_type(ctx, field)
case "name": case "name":
return ec.fieldContext_Tag_name(ctx, field) return ec.fieldContext_Tag_name(ctx, field)
case "scope":
return ec.fieldContext_Tag_scope(ctx, field)
} }
return nil, fmt.Errorf("no field named %q was found under type Tag", field.Name) return nil, fmt.Errorf("no field named %q was found under type Tag", field.Name)
}, },
@ -7829,6 +7851,8 @@ func (ec *executionContext) fieldContext_Mutation_addTagsToJob(ctx context.Conte
return ec.fieldContext_Tag_type(ctx, field) return ec.fieldContext_Tag_type(ctx, field)
case "name": case "name":
return ec.fieldContext_Tag_name(ctx, field) return ec.fieldContext_Tag_name(ctx, field)
case "scope":
return ec.fieldContext_Tag_scope(ctx, field)
} }
return nil, fmt.Errorf("no field named %q was found under type Tag", field.Name) return nil, fmt.Errorf("no field named %q was found under type Tag", field.Name)
}, },
@ -7892,6 +7916,8 @@ func (ec *executionContext) fieldContext_Mutation_removeTagsFromJob(ctx context.
return ec.fieldContext_Tag_type(ctx, field) return ec.fieldContext_Tag_type(ctx, field)
case "name": case "name":
return ec.fieldContext_Tag_name(ctx, field) return ec.fieldContext_Tag_name(ctx, field)
case "scope":
return ec.fieldContext_Tag_scope(ctx, field)
} }
return nil, fmt.Errorf("no field named %q was found under type Tag", field.Name) return nil, fmt.Errorf("no field named %q was found under type Tag", field.Name)
}, },
@ -8199,6 +8225,8 @@ func (ec *executionContext) fieldContext_Query_tags(_ context.Context, field gra
return ec.fieldContext_Tag_type(ctx, field) return ec.fieldContext_Tag_type(ctx, field)
case "name": case "name":
return ec.fieldContext_Tag_name(ctx, field) return ec.fieldContext_Tag_name(ctx, field)
case "scope":
return ec.fieldContext_Tag_scope(ctx, field)
} }
return nil, fmt.Errorf("no field named %q was found under type Tag", field.Name) return nil, fmt.Errorf("no field named %q was found under type Tag", field.Name)
}, },
@ -10547,6 +10575,50 @@ func (ec *executionContext) fieldContext_Tag_name(_ context.Context, field graph
return fc, nil return fc, nil
} }
func (ec *executionContext) _Tag_scope(ctx context.Context, field graphql.CollectedField, obj *schema.Tag) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Tag_scope(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 obj.Scope, nil
})
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.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Tag_scope(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Tag",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _TimeRangeOutput_from(ctx context.Context, field graphql.CollectedField, obj *model.TimeRangeOutput) (ret graphql.Marshaler) { func (ec *executionContext) _TimeRangeOutput_from(ctx context.Context, field graphql.CollectedField, obj *model.TimeRangeOutput) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_TimeRangeOutput_from(ctx, field) fc, err := ec.fieldContext_TimeRangeOutput_from(ctx, field)
if err != nil { if err != nil {
@ -15666,6 +15738,11 @@ func (ec *executionContext) _Tag(ctx context.Context, sel ast.SelectionSet, obj
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
out.Invalids++ out.Invalids++
} }
case "scope":
out.Values[i] = ec._Tag_scope(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }

View File

@ -29,7 +29,7 @@ func (r *clusterResolver) Partitions(ctx context.Context, obj *schema.Cluster) (
// Tags is the resolver for the tags field. // Tags is the resolver for the tags field.
func (r *jobResolver) Tags(ctx context.Context, obj *schema.Job) ([]*schema.Tag, error) { func (r *jobResolver) Tags(ctx context.Context, obj *schema.Job) ([]*schema.Tag, error) {
return r.Repo.GetTags(&obj.ID) return r.Repo.GetTags(ctx, &obj.ID)
} }
// ConcurrentJobs is the resolver for the concurrentJobs field. // ConcurrentJobs is the resolver for the concurrentJobs field.
@ -86,14 +86,14 @@ func (r *metricValueResolver) Name(ctx context.Context, obj *schema.MetricValue)
} }
// CreateTag is the resolver for the createTag field. // CreateTag is the resolver for the createTag field.
func (r *mutationResolver) CreateTag(ctx context.Context, typeArg string, name string) (*schema.Tag, error) { func (r *mutationResolver) CreateTag(ctx context.Context, typeArg string, name string, scope string) (*schema.Tag, error) {
id, err := r.Repo.CreateTag(typeArg, name) id, err := r.Repo.CreateTag(typeArg, name, scope)
if err != nil { if err != nil {
log.Warn("Error while creating tag") log.Warn("Error while creating tag")
return nil, err return nil, err
} }
return &schema.Tag{ID: id, Type: typeArg, Name: name}, nil return &schema.Tag{ID: id, Type: typeArg, Name: name, Scope: scope}, nil
} }
// DeleteTag is the resolver for the deleteTag field. // DeleteTag is the resolver for the deleteTag field.
@ -103,6 +103,7 @@ func (r *mutationResolver) DeleteTag(ctx context.Context, id string) (string, er
// AddTagsToJob is the resolver for the addTagsToJob field. // AddTagsToJob is the resolver for the addTagsToJob field.
func (r *mutationResolver) AddTagsToJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) { func (r *mutationResolver) AddTagsToJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) {
// Selectable Tags Pre-Filtered by Scope in Frontend: No backend check required
jid, err := strconv.ParseInt(job, 10, 64) jid, err := strconv.ParseInt(job, 10, 64)
if err != nil { if err != nil {
log.Warn("Error while adding tag to job") log.Warn("Error while adding tag to job")
@ -117,7 +118,7 @@ func (r *mutationResolver) AddTagsToJob(ctx context.Context, job string, tagIds
return nil, err return nil, err
} }
if tags, err = r.Repo.AddTag(jid, tid); err != nil { if tags, err = r.Repo.AddTag(ctx, jid, tid); err != nil {
log.Warn("Error while adding tag") log.Warn("Error while adding tag")
return nil, err return nil, err
} }
@ -128,6 +129,7 @@ func (r *mutationResolver) AddTagsToJob(ctx context.Context, job string, tagIds
// RemoveTagsFromJob is the resolver for the removeTagsFromJob field. // RemoveTagsFromJob is the resolver for the removeTagsFromJob field.
func (r *mutationResolver) RemoveTagsFromJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) { func (r *mutationResolver) RemoveTagsFromJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) {
// Removable Tags Pre-Filtered by Scope in Frontend: No backend check required
jid, err := strconv.ParseInt(job, 10, 64) jid, err := strconv.ParseInt(job, 10, 64)
if err != nil { if err != nil {
log.Warn("Error while parsing job id") log.Warn("Error while parsing job id")
@ -142,7 +144,7 @@ func (r *mutationResolver) RemoveTagsFromJob(ctx context.Context, job string, ta
return nil, err return nil, err
} }
if tags, err = r.Repo.RemoveTag(jid, tid); err != nil { if tags, err = r.Repo.RemoveTag(ctx, jid, tid); err != nil {
log.Warn("Error while removing tag") log.Warn("Error while removing tag")
return nil, err return nil, err
} }
@ -168,7 +170,7 @@ func (r *queryResolver) Clusters(ctx context.Context) ([]*schema.Cluster, error)
// Tags is the resolver for the tags field. // Tags is the resolver for the tags field.
func (r *queryResolver) Tags(ctx context.Context) ([]*schema.Tag, error) { func (r *queryResolver) Tags(ctx context.Context) ([]*schema.Tag, error) {
return r.Repo.GetTags(nil) return r.Repo.GetTags(ctx, nil)
} }
// GlobalMetrics is the resolver for the globalMetrics field. // GlobalMetrics is the resolver for the globalMetrics field.

View File

@ -112,8 +112,8 @@ func HandleImportFlag(flag string) error {
} }
for _, tag := range job.Tags { for _, tag := range job.Tags {
if _, err := r.AddTagOrCreate(id, tag.Type, tag.Name); err != nil { if err := r.ImportTag(id, tag.Type, tag.Name, tag.Scope); err != nil {
log.Error("Error while adding or creating tag") log.Error("Error while adding or creating tag on import")
return err return err
} }
} }

View File

@ -5,6 +5,8 @@
package repository package repository
import ( import (
"context"
"fmt"
"strings" "strings"
"github.com/ClusterCockpit/cc-backend/pkg/archive" "github.com/ClusterCockpit/cc-backend/pkg/archive"
@ -14,7 +16,14 @@ import (
) )
// Add the tag with id `tagId` to the job with the database id `jobId`. // Add the tag with id `tagId` to the job with the database id `jobId`.
func (r *JobRepository) AddTag(job int64, tag int64) ([]*schema.Tag, error) { func (r *JobRepository) AddTag(ctx context.Context, job int64, tag int64) ([]*schema.Tag, error) {
j, err := r.FindById(ctx, job)
if err != nil {
log.Warn("Error while finding job by id")
return nil, err
}
q := sq.Insert("jobtag").Columns("job_id", "tag_id").Values(job, tag) q := sq.Insert("jobtag").Columns("job_id", "tag_id").Values(job, tag)
if _, err := q.RunWith(r.stmtCache).Exec(); err != nil { if _, err := q.RunWith(r.stmtCache).Exec(); err != nil {
@ -23,49 +32,56 @@ func (r *JobRepository) AddTag(job int64, tag int64) ([]*schema.Tag, error) {
return nil, err return nil, err
} }
j, err := r.FindByIdDirect(job) tags, err := r.GetTags(ctx, &job)
if err != nil {
log.Warn("Error while finding job by id")
return nil, err
}
tags, err := r.GetTags(&job)
if err != nil { if err != nil {
log.Warn("Error while getting tags for job") log.Warn("Error while getting tags for job")
return nil, err return nil, err
} }
return tags, archive.UpdateTags(j, tags) archiveTags, err := r.getArchiveTags(&job)
if err != nil {
log.Warn("Error while getting tags for job")
return nil, err
}
return tags, archive.UpdateTags(j, archiveTags)
} }
// Removes a tag from a job // Removes a tag from a job
func (r *JobRepository) RemoveTag(job, tag int64) ([]*schema.Tag, error) { func (r *JobRepository) RemoveTag(ctx context.Context, job, tag int64) ([]*schema.Tag, error) {
j, err := r.FindById(ctx, job)
if err != nil {
log.Warn("Error while finding job by id")
return nil, err
}
q := sq.Delete("jobtag").Where("jobtag.job_id = ?", job).Where("jobtag.tag_id = ?", tag) q := sq.Delete("jobtag").Where("jobtag.job_id = ?", job).Where("jobtag.tag_id = ?", tag)
if _, err := q.RunWith(r.stmtCache).Exec(); err != nil { if _, err := q.RunWith(r.stmtCache).Exec(); err != nil {
s, _, _ := q.ToSql() s, _, _ := q.ToSql()
log.Errorf("Error adding tag with %s: %v", s, err) log.Errorf("Error removing tag with %s: %v", s, err)
return nil, err return nil, err
} }
j, err := r.FindByIdDirect(job) tags, err := r.GetTags(ctx, &job)
if err != nil {
log.Warn("Error while finding job by id")
return nil, err
}
tags, err := r.GetTags(&job)
if err != nil { if err != nil {
log.Warn("Error while getting tags for job") log.Warn("Error while getting tags for job")
return nil, err return nil, err
} }
return tags, archive.UpdateTags(j, tags) archiveTags, err := r.getArchiveTags(&job)
if err != nil {
log.Warn("Error while getting tags for job")
return nil, err
}
return tags, archive.UpdateTags(j, archiveTags)
} }
// CreateTag creates a new tag with the specified type and name and returns its database id. // CreateTag creates a new tag with the specified type and name and returns its database id.
func (r *JobRepository) CreateTag(tagType string, tagName string) (tagId int64, err error) { func (r *JobRepository) CreateTag(tagType string, tagName string, tagScope string) (tagId int64, err error) {
q := sq.Insert("tag").Columns("tag_type", "tag_name").Values(tagType, tagName) q := sq.Insert("tag").Columns("tag_type", "tag_name", "tag_scope").Values(tagType, tagName, tagScope)
res, err := q.RunWith(r.stmtCache).Exec() res, err := q.RunWith(r.stmtCache).Exec()
if err != nil { if err != nil {
@ -77,9 +93,10 @@ func (r *JobRepository) CreateTag(tagType string, tagName string) (tagId int64,
return res.LastInsertId() return res.LastInsertId()
} }
func (r *JobRepository) CountTags(user *schema.User) (tags []schema.Tag, counts map[string]int, err error) { func (r *JobRepository) CountTags(ctx context.Context) (tags []schema.Tag, counts map[string]int, err error) {
// Fetch all Tags in DB for Display in Frontend Tag-View
tags = make([]schema.Tag, 0, 100) tags = make([]schema.Tag, 0, 100)
xrows, err := r.DB.Queryx("SELECT id, tag_type, tag_name FROM tag") xrows, err := r.DB.Queryx("SELECT id, tag_type, tag_name, tag_scope FROM tag")
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -89,14 +106,36 @@ func (r *JobRepository) CountTags(user *schema.User) (tags []schema.Tag, counts
if err = xrows.StructScan(&t); err != nil { if err = xrows.StructScan(&t); err != nil {
return nil, nil, err return nil, nil, err
} }
tags = append(tags, t)
// Handle Scope Filtering: Tag Scope is Global, Private (== Username) or User is auth'd to view Admin Tags
readable, err := r.checkScopeAuth(ctx, "read", t.Scope)
if err != nil {
return nil, nil, err
}
if readable {
tags = append(tags, t)
}
} }
user := GetUserFromContext(ctx)
// Query and Count Jobs with attached Tags
q := sq.Select("t.tag_name, count(jt.tag_id)"). q := sq.Select("t.tag_name, count(jt.tag_id)").
From("tag t"). From("tag t").
LeftJoin("jobtag jt ON t.id = jt.tag_id"). LeftJoin("jobtag jt ON t.id = jt.tag_id").
GroupBy("t.tag_name") GroupBy("t.tag_name")
// Handle Scope Filtering
scopeList := "\"global\""
if user != nil {
scopeList += ",\"" + user.Username + "\""
}
if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) {
scopeList += ",\"admin\""
}
q = q.Where("t.tag_scope IN (" + scopeList + ")")
// Handle Job Ownership
if user != nil && user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) { // ADMIN || SUPPORT: Count all jobs if user != nil && user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) { // ADMIN || SUPPORT: Count all jobs
log.Debug("CountTags: User Admin or Support -> Count all Jobs for Tags") log.Debug("CountTags: User Admin or Support -> Count all Jobs for Tags")
// Unchanged: Needs to be own case still, due to UserRole/NoRole compatibility handling in else case // Unchanged: Needs to be own case still, due to UserRole/NoRole compatibility handling in else case
@ -123,21 +162,30 @@ func (r *JobRepository) CountTags(user *schema.User) (tags []schema.Tag, counts
} }
err = rows.Err() err = rows.Err()
return return tags, counts, err
} }
// AddTagOrCreate adds the tag with the specified type and name to the job with the database id `jobId`. // AddTagOrCreate adds the tag with the specified type and name to the job with the database id `jobId`.
// If such a tag does not yet exist, it is created. // If such a tag does not yet exist, it is created.
func (r *JobRepository) AddTagOrCreate(jobId int64, tagType string, tagName string) (tagId int64, err error) { func (r *JobRepository) AddTagOrCreate(ctx context.Context, jobId int64, tagType string, tagName string, tagScope string) (tagId int64, err error) {
tagId, exists := r.TagId(tagType, tagName)
writable, err := r.checkScopeAuth(ctx, "write", tagScope)
if err != nil {
return 0, err
}
if !writable {
return 0, fmt.Errorf("cannot write tag scope with current authorization")
}
tagId, exists := r.TagId(tagType, tagName, tagScope)
if !exists { if !exists {
tagId, err = r.CreateTag(tagType, tagName) tagId, err = r.CreateTag(tagType, tagName, tagScope)
if err != nil { if err != nil {
return 0, err return 0, err
} }
} }
if _, err := r.AddTag(jobId, tagId); err != nil { if _, err := r.AddTag(ctx, jobId, tagId); err != nil {
return 0, err return 0, err
} }
@ -145,19 +193,19 @@ func (r *JobRepository) AddTagOrCreate(jobId int64, tagType string, tagName stri
} }
// TagId returns the database id of the tag with the specified type and name. // TagId returns the database id of the tag with the specified type and name.
func (r *JobRepository) TagId(tagType string, tagName string) (tagId int64, exists bool) { func (r *JobRepository) TagId(tagType string, tagName string, tagScope string) (tagId int64, exists bool) {
exists = true exists = true
if err := sq.Select("id").From("tag"). if err := sq.Select("id").From("tag").
Where("tag.tag_type = ?", tagType).Where("tag.tag_name = ?", tagName). Where("tag.tag_type = ?", tagType).Where("tag.tag_name = ?", tagName).Where("tag.tag_scope = ?", tagScope).
RunWith(r.stmtCache).QueryRow().Scan(&tagId); err != nil { RunWith(r.stmtCache).QueryRow().Scan(&tagId); err != nil {
exists = false exists = false
} }
return return
} }
// GetTags returns a list of all tags if job is nil or of the tags that the job with that database ID has. // GetTags returns a list of all scoped tags if job is nil or of the tags that the job with that database ID has.
func (r *JobRepository) GetTags(job *int64) ([]*schema.Tag, error) { func (r *JobRepository) GetTags(ctx context.Context, job *int64) ([]*schema.Tag, error) {
q := sq.Select("id", "tag_type", "tag_name").From("tag") q := sq.Select("id", "tag_type", "tag_name", "tag_scope").From("tag")
if job != nil { if job != nil {
q = q.Join("jobtag ON jobtag.tag_id = tag.id").Where("jobtag.job_id = ?", *job) q = q.Join("jobtag ON jobtag.tag_id = tag.id").Where("jobtag.job_id = ?", *job)
} }
@ -172,7 +220,41 @@ func (r *JobRepository) GetTags(job *int64) ([]*schema.Tag, error) {
tags := make([]*schema.Tag, 0) tags := make([]*schema.Tag, 0)
for rows.Next() { for rows.Next() {
tag := &schema.Tag{} tag := &schema.Tag{}
if err := rows.Scan(&tag.ID, &tag.Type, &tag.Name); err != nil { if err := rows.Scan(&tag.ID, &tag.Type, &tag.Name, &tag.Scope); err != nil {
log.Warn("Error while scanning rows")
return nil, err
}
// Handle Scope Filtering: Tag Scope is Global, Private (== Username) or User is auth'd to view Admin Tags
readable, err := r.checkScopeAuth(ctx, "read", tag.Scope)
if err != nil {
return nil, err
}
if readable {
tags = append(tags, tag)
}
}
return tags, nil
}
// GetArchiveTags returns a list of all tags *regardless of scope* if job is nil or of the tags that the job with that database ID has.
func (r *JobRepository) getArchiveTags(job *int64) ([]*schema.Tag, error) {
q := sq.Select("id", "tag_type", "tag_name", "tag_scope").From("tag")
if job != nil {
q = q.Join("jobtag ON jobtag.tag_id = tag.id").Where("jobtag.job_id = ?", *job)
}
rows, err := q.RunWith(r.stmtCache).Query()
if err != nil {
s, _, _ := q.ToSql()
log.Errorf("Error get tags with %s: %v", s, err)
return nil, err
}
tags := make([]*schema.Tag, 0)
for rows.Next() {
tag := &schema.Tag{}
if err := rows.Scan(&tag.ID, &tag.Type, &tag.Name, &tag.Scope); err != nil {
log.Warn("Error while scanning rows") log.Warn("Error while scanning rows")
return nil, err return nil, err
} }
@ -181,3 +263,60 @@ func (r *JobRepository) GetTags(job *int64) ([]*schema.Tag, error) {
return tags, nil return tags, nil
} }
func (r *JobRepository) ImportTag(jobId int64, tagType string, tagName string, tagScope string) (err error) {
// Import has no scope ctx, only import from metafile to DB (No recursive archive update required), only returns err
tagId, exists := r.TagId(tagType, tagName, tagScope)
if !exists {
tagId, err = r.CreateTag(tagType, tagName, tagScope)
if err != nil {
return err
}
}
q := sq.Insert("jobtag").Columns("job_id", "tag_id").Values(jobId, tagId)
if _, err := q.RunWith(r.stmtCache).Exec(); err != nil {
s, _, _ := q.ToSql()
log.Errorf("Error adding tag on import with %s: %v", s, err)
return err
}
return nil
}
func (r *JobRepository) checkScopeAuth(ctx context.Context, operation string, scope string) (pass bool, err error) {
user := GetUserFromContext(ctx)
if user != nil {
switch {
case operation == "write" && scope == "admin":
if user.HasRole(schema.RoleAdmin) || (len(user.Roles) == 1 && user.HasRole(schema.RoleApi)) {
return true, nil
}
return false, nil
case operation == "write" && scope == "global":
if user.HasRole(schema.RoleAdmin) || (len(user.Roles) == 1 && user.HasRole(schema.RoleApi)) {
return true, nil
}
return false, nil
case operation == "write" && scope == user.Username:
return true, nil
case operation == "read" && scope == "admin":
return user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}), nil
case operation == "read" && scope == "global":
return true, nil
case operation == "read" && scope == user.Username:
return true, nil
default:
if operation == "read" || operation == "write" {
// No acceptable scope: deny tag
return false, nil
} else {
return false, fmt.Errorf("error while checking tag operation auth: unknown operation (%s)", operation)
}
}
} else {
return false, fmt.Errorf("error while checking tag operation auth: no user in context")
}
}

View File

@ -124,9 +124,8 @@ func setupAnalysisRoute(i InfoType, r *http.Request) InfoType {
func setupTaglistRoute(i InfoType, r *http.Request) InfoType { func setupTaglistRoute(i InfoType, r *http.Request) InfoType {
jobRepo := repository.GetJobRepository() jobRepo := repository.GetJobRepository()
user := repository.GetUserFromContext(r.Context())
tags, counts, err := jobRepo.CountTags(user) tags, counts, err := jobRepo.CountTags(r.Context())
tagMap := make(map[string][]map[string]interface{}) tagMap := make(map[string][]map[string]interface{})
if err != nil { if err != nil {
log.Warnf("GetTags failed: %s", err.Error()) log.Warnf("GetTags failed: %s", err.Error())
@ -138,6 +137,7 @@ func setupTaglistRoute(i InfoType, r *http.Request) InfoType {
tagItem := map[string]interface{}{ tagItem := map[string]interface{}{
"id": tag.ID, "id": tag.ID,
"name": tag.Name, "name": tag.Name,
"scope": tag.Scope,
"count": counts[tag.Name], "count": counts[tag.Name],
} }
tagMap[tag.Type] = append(tagMap[tag.Type], tagItem) tagMap[tag.Type] = append(tagMap[tag.Type], tagItem)

View File

@ -171,8 +171,9 @@ func UpdateTags(job *schema.Job, tags []*schema.Tag) error {
jobMeta.Tags = make([]*schema.Tag, 0) jobMeta.Tags = make([]*schema.Tag, 0)
for _, tag := range tags { for _, tag := range tags {
jobMeta.Tags = append(jobMeta.Tags, &schema.Tag{ jobMeta.Tags = append(jobMeta.Tags, &schema.Tag{
Name: tag.Name, Name: tag.Name,
Type: tag.Type, Type: tag.Type,
Scope: tag.Scope,
}) })
} }

View File

@ -117,9 +117,10 @@ type JobStatistics struct {
// Tag model // Tag model
// @Description Defines a tag using name and type. // @Description Defines a tag using name and type.
type Tag struct { type Tag struct {
Type string `json:"type" db:"tag_type" example:"Debug"` Type string `json:"type" db:"tag_type" example:"Debug"`
Name string `json:"name" db:"tag_name" example:"Testjob"` Name string `json:"name" db:"tag_name" example:"Testjob"`
ID int64 `json:"id" db:"id"` Scope string `json:"scope" db:"tag_scope" example:"global"`
ID int64 `json:"id" db:"id"`
} }
// Resource model // Resource model

View File

@ -2,9 +2,9 @@
@component Main single job display component; displays plots for every metric as well as various information @component Main single job display component; displays plots for every metric as well as various information
Properties: Properties:
- `dbid Number`: The jobs DB ID
- `username String`: Empty string if auth. is disabled, otherwise the username as string - `username String`: Empty string if auth. is disabled, otherwise the username as string
- `authlevel Number`: The current users authentication level - `authlevel Number`: The current users authentication level
- `clusters [String]`: List of cluster names
- `roles [Number]`: Enum containing available roles - `roles [Number]`: Enum containing available roles
--> -->
@ -45,6 +45,7 @@
import MetricSelection from "./generic/select/MetricSelection.svelte"; import MetricSelection from "./generic/select/MetricSelection.svelte";
export let dbid; export let dbid;
export let username;
export let authlevel; export let authlevel;
export let roles; export let roles;
@ -58,8 +59,7 @@
selectedScopes = []; selectedScopes = [];
let plots = {}, let plots = {},
jobTags, jobTags
statsTable
let missingMetrics = [], let missingMetrics = [],
missingHosts = [], missingHosts = [],
@ -322,7 +322,7 @@
<Row class="mb-3"> <Row class="mb-3">
<Col xs="auto"> <Col xs="auto">
{#if $initq.data} {#if $initq.data}
<TagManagement job={$initq.data.job} bind:jobTags /> <TagManagement job={$initq.data.job} {username} {authlevel} {roles} bind:jobTags />
{/if} {/if}
</Col> </Col>
<Col xs="auto"> <Col xs="auto">
@ -418,7 +418,6 @@
{#if $jobMetrics?.data?.jobMetrics} {#if $jobMetrics?.data?.jobMetrics}
{#key $jobMetrics.data.jobMetrics} {#key $jobMetrics.data.jobMetrics}
<StatsTable <StatsTable
bind:this={statsTable}
job={$initq.data.job} job={$initq.data.job}
jobMetrics={$jobMetrics.data.jobMetrics} jobMetrics={$jobMetrics.data.jobMetrics}
/> />

View File

@ -27,7 +27,7 @@
import { import {
init, init,
checkMetricDisabled, checkMetricDisabled,
} from "./utils.js"; } from "./generic/utils.js";
import PlotTable from "./generic/PlotTable.svelte"; import PlotTable from "./generic/PlotTable.svelte";
import MetricPlot from "./generic/plots/MetricPlot.svelte"; import MetricPlot from "./generic/plots/MetricPlot.svelte";
import TimeSelection from "./generic/select/TimeSelection.svelte"; import TimeSelection from "./generic/select/TimeSelection.svelte";

View File

@ -81,6 +81,7 @@
id id
type type
name name
scope
} }
userData { userData {
name name

View File

@ -37,7 +37,13 @@
<a target={clickable ? "_blank" : null} href={clickable ? `/monitoring/jobs/?tag=${id}` : null}> <a target={clickable ? "_blank" : null} href={clickable ? `/monitoring/jobs/?tag=${id}` : null}>
{#if tag} {#if tag}
<span class="badge bg-warning text-dark">{tag.type}: {tag.name}</span> {#if tag?.scope === "global"}
<span style="background-color:#9e9e9e;" class="badge text-dark">{tag.type}: {tag.name}</span>
{:else if tag.scope === "admin"}
<span style="background-color:#80deea;" class="badge text-dark">{tag.type}: {tag.name}</span>
{:else}
<span class="badge bg-warning text-dark">{tag.type}: {tag.name}</span>
{/if}
{:else} {:else}
Loading... Loading...
{/if} {/if}

View File

@ -77,7 +77,7 @@ export function init(extraInitQuery = "") {
footprint footprint
} }
} }
tags { id, name, type } tags { id, name, type, scope }
globalMetrics { globalMetrics {
name name
scope scope

View File

@ -5,6 +5,7 @@ new Job({
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
dbid: jobInfos.id, dbid: jobInfos.id,
username: username,
authlevel: authlevel, authlevel: authlevel,
roles: roles roles: roles
}, },

View File

@ -4,6 +4,9 @@
Properties: Properties:
- `job Object`: The job object - `job Object`: The job object
- `jobTags [Number]`: The array of currently designated tags - `jobTags [Number]`: The array of currently designated tags
- `username String`: Empty string if auth. is disabled, otherwise the username as string
- `authlevel Number`: The current users authentication level
- `roles [Number]`: Enum containing available roles
--> -->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
@ -25,30 +28,35 @@
export let job; export let job;
export let jobTags = job.tags; export let jobTags = job.tags;
export let username;
export let authlevel;
export let roles;
let allTags = getContext("tags"), let allTags = getContext("tags"),
initialized = getContext("initialized"); initialized = getContext("initialized");
let newTagType = "", let newTagType = "",
newTagName = ""; newTagName = "",
newTagScope = username;
let filterTerm = ""; let filterTerm = "";
let pendingChange = false; let pendingChange = false;
let isOpen = false; let isOpen = false;
const client = getContextClient(); const client = getContextClient();
const createTagMutation = ({ type, name }) => { const createTagMutation = ({ type, name, scope }) => {
return mutationStore({ return mutationStore({
client: client, client: client,
query: gql` query: gql`
mutation ($type: String!, $name: String!) { mutation ($type: String!, $name: String!, $scope: String!) {
createTag(type: $type, name: $name) { createTag(type: $type, name: $name, scope: $scope) {
id id
type type
name name
scope
} }
} }
`, `,
variables: { type, name }, variables: { type, name, scope },
}); });
}; };
@ -61,6 +69,7 @@
id id
type type
name name
scope
} }
} }
`, `,
@ -77,6 +86,7 @@
id id
type type
name name
scope
} }
} }
`, `,
@ -103,9 +113,9 @@
return true; return true;
} }
function createTag(type, name) { function createTag(type, name, scope) {
pendingChange = true; pendingChange = true;
createTagMutation({ type: type, name: name }).subscribe((res) => { createTagMutation({ type: type, name: name, scope: scope }).subscribe((res) => {
if (res.fetching === false && !res.error) { if (res.fetching === false && !res.error) {
pendingChange = false; pendingChange = false;
allTags = [...allTags, res.data.createTag]; allTags = [...allTags, res.data.createTag];
@ -206,16 +216,30 @@
</ul> </ul>
<br /> <br />
{#if newTagType && newTagName && isNewTag(newTagType, newTagName)} {#if newTagType && newTagName && isNewTag(newTagType, newTagName)}
<Button <div class="d-flex">
outline <Button
color="success" style="margin-right: 10px;"
on:click={(e) => ( outline
e.preventDefault(), createTag(newTagType, newTagName) color="success"
)} on:click={(e) => (
> e.preventDefault(), createTag(newTagType, newTagName, newTagScope)
Create & Add Tag: )}
<Tag tag={{ type: newTagType, name: newTagName }} clickable={false} /> >
</Button> Create & Add Tag:
<Tag tag={{ type: newTagType, name: newTagName, scope: newTagScope }} clickable={false}/>
</Button>
{#if roles && authlevel >= roles.admin}
<select
style="max-width: 175px;"
class="form-select"
bind:value={newTagScope}
>
<option value={username}>Scope: Private</option>
<option value={"global"}>Scope: Global</option>
<option value={"admin"}>Scope: Admin</option>
</select>
{/if}
</div>
{:else if allTagsFiltered.length == 0} {:else if allTagsFiltered.length == 0}
<Alert>Search Term is not a valid Tag (<code>type: name</code>)</Alert> <Alert>Search Term is not a valid Tag (<code>type: name</code>)</Alert>
{/if} {/if}

View File

@ -11,6 +11,7 @@
id: "{{ .Infos.id }}", id: "{{ .Infos.id }}",
}; };
const clusterCockpitConfig = {{ .Config }}; const clusterCockpitConfig = {{ .Config }};
const username = {{ .User.Username }};
const authlevel = {{ .User.GetAuthLevel }}; const authlevel = {{ .User.GetAuthLevel }};
const roles = {{ .Roles }}; const roles = {{ .Roles }};
</script> </script>

View File

@ -7,8 +7,16 @@
{{ $tagType }} {{ $tagType }}
</div> </div>
{{ range $tagList }} {{ range $tagList }}
<a class="btn btn-lg btn-warning" href="/monitoring/jobs/?tag={{ .id }}" role="button"> {{if eq .scope "global"}}
{{ .name }} <span class="badge bg-light text-dark">{{ .count }}</span> </a> <a style="background-color:#9e9e9e;" class="btn btn-lg" href="/monitoring/jobs/?tag={{ .id }}" role="button">
{{ .name }} <span class="badge bg-light text-dark">{{ .count }}</span> </a>
{{else if eq .scope "admin"}}
<a style="background-color:#80deea;" class="btn btn-lg" href="/monitoring/jobs/?tag={{ .id }}" role="button">
{{ .name }} <span class="badge bg-light text-dark">{{ .count }}</span> </a>
{{else}}
<a class="btn btn-lg btn-warning" href="/monitoring/jobs/?tag={{ .id }}" role="button">
{{ .name }} <span class="badge bg-light text-dark">{{ .count }}</span> </a>
{{end}}
{{end}} {{end}}
{{end}} {{end}}
</div> </div>