diff --git a/internal/importer/handleImport.go b/internal/importer/handleImport.go index 4ef9f84..81a312f 100644 --- a/internal/importer/handleImport.go +++ b/internal/importer/handleImport.go @@ -10,7 +10,6 @@ import ( "fmt" "os" "strings" - "time" "github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/internal/repository" @@ -43,8 +42,8 @@ func HandleImportFlag(flag string) error { } dec := json.NewDecoder(bytes.NewReader(raw)) dec.DisallowUnknownFields() - jobMeta := schema.JobMeta{BaseJob: schema.JobDefaults} - if err = dec.Decode(&jobMeta); err != nil { + job := schema.JobMeta{BaseJob: schema.JobDefaults} + if err = dec.Decode(&job); err != nil { log.Warn("Error while decoding raw json metadata for import") return err } @@ -68,26 +67,9 @@ func HandleImportFlag(flag string) error { return err } - // checkJobData(&jobData) + job.MonitoringStatus = schema.MonitoringStatusArchivingSuccessful - jobMeta.MonitoringStatus = schema.MonitoringStatusArchivingSuccessful - - // if _, err = r.Find(&jobMeta.JobID, &jobMeta.Cluster, &jobMeta.StartTime); err != sql.ErrNoRows { - // if err != nil { - // log.Warn("Error while finding job in jobRepository") - // return err - // } - // - // return fmt.Errorf("REPOSITORY/INIT > a job with that jobId, cluster and startTime does already exist") - // } - // - job := schema.Job{ - BaseJob: jobMeta.BaseJob, - StartTime: time.Unix(jobMeta.StartTime, 0), - StartTimeUnix: jobMeta.StartTime, - } - - sc, err := archive.GetSubCluster(jobMeta.Cluster, jobMeta.SubCluster) + sc, err := archive.GetSubCluster(job.Cluster, job.SubCluster) if err != nil { log.Errorf("cannot get subcluster: %s", err.Error()) return err @@ -96,14 +78,13 @@ func HandleImportFlag(flag string) error { job.Footprint = make(map[string]float64) for _, fp := range sc.Footprint { - job.Footprint[fp] = util.LoadJobStat(&jobMeta, fp) + job.Footprint[fp] = util.LoadJobStat(&job, fp) } job.RawFootprint, err = json.Marshal(job.Footprint) if err != nil { log.Warn("Error while marshaling job footprint") return err } - job.RawResources, err = json.Marshal(job.Resources) if err != nil { log.Warn("Error while marshaling job resources") @@ -120,7 +101,7 @@ func HandleImportFlag(flag string) error { return err } - if err = archive.GetHandle().ImportJob(&jobMeta, &jobData); err != nil { + if err = archive.GetHandle().ImportJob(&job, &jobData); err != nil { log.Error("Error while importing job") return err } diff --git a/internal/repository/job.go b/internal/repository/job.go index 9438edd..8dda6e0 100644 --- a/internal/repository/job.go +++ b/internal/repository/job.go @@ -222,230 +222,6 @@ func (r *JobRepository) UpdateMetadata(job *schema.Job, key, val string) (err er return archive.UpdateMetadata(job, job.MetaData) } -// Find executes a SQL query to find a specific batch job. -// The job is queried using the batch job id, the cluster name, -// and the start time of the job in UNIX epoch time seconds. -// It returns a pointer to a schema.Job data structure and an error variable. -// To check if no job was found test err == sql.ErrNoRows -func (r *JobRepository) Find( - jobId *int64, - cluster *string, - startTime *int64, -) (*schema.Job, error) { - start := time.Now() - q := sq.Select(jobColumns...).From("job"). - Where("job.job_id = ?", *jobId) - - if cluster != nil { - q = q.Where("job.cluster = ?", *cluster) - } - if startTime != nil { - q = q.Where("job.start_time = ?", *startTime) - } - - log.Debugf("Timer Find %s", time.Since(start)) - - return scanJob(q.RunWith(r.stmtCache).QueryRow()) -} - -// Find executes a SQL query to find a specific batch job. -// The job is queried using the batch job id, the cluster name, -// and the start time of the job in UNIX epoch time seconds. -// It returns a pointer to a schema.Job data structure and an error variable. -// To check if no job was found test err == sql.ErrNoRows -func (r *JobRepository) FindAll( - jobId *int64, - cluster *string, - startTime *int64, -) ([]*schema.Job, error) { - start := time.Now() - q := sq.Select(jobColumns...).From("job"). - Where("job.job_id = ?", *jobId) - - if cluster != nil { - q = q.Where("job.cluster = ?", *cluster) - } - if startTime != nil { - q = q.Where("job.start_time = ?", *startTime) - } - - rows, err := q.RunWith(r.stmtCache).Query() - if err != nil { - log.Error("Error while running query") - return nil, err - } - - jobs := make([]*schema.Job, 0, 10) - for rows.Next() { - job, err := scanJob(rows) - if err != nil { - log.Warn("Error while scanning rows") - return nil, err - } - jobs = append(jobs, job) - } - log.Debugf("Timer FindAll %s", time.Since(start)) - return jobs, nil -} - -// FindById executes a SQL query to find a specific batch job. -// The job is queried using the database id. -// It returns a pointer to a schema.Job data structure and an error variable. -// To check if no job was found test err == sql.ErrNoRows -func (r *JobRepository) FindById(jobId int64) (*schema.Job, error) { - q := sq.Select(jobColumns...). - From("job").Where("job.id = ?", jobId) - return scanJob(q.RunWith(r.stmtCache).QueryRow()) -} - -func (r *JobRepository) FindConcurrentJobs( - ctx context.Context, - job *schema.Job, -) (*model.JobLinkResultList, error) { - if job == nil { - return nil, nil - } - - query, qerr := SecurityCheck(ctx, sq.Select("job.id", "job.job_id", "job.start_time").From("job")) - if qerr != nil { - return nil, qerr - } - - query = query.Where("cluster = ?", job.Cluster) - var startTime int64 - var stopTime int64 - - startTime = job.StartTimeUnix - hostname := job.Resources[0].Hostname - - if job.State == schema.JobStateRunning { - stopTime = time.Now().Unix() - } else { - stopTime = startTime + int64(job.Duration) - } - - // Add 200s overlap for jobs start time at the end - startTimeTail := startTime + 10 - stopTimeTail := stopTime - 200 - startTimeFront := startTime + 200 - - queryRunning := query.Where("job.job_state = ?").Where("(job.start_time BETWEEN ? AND ? OR job.start_time < ?)", - "running", startTimeTail, stopTimeTail, startTime) - queryRunning = queryRunning.Where("job.resources LIKE ?", fmt.Sprint("%", hostname, "%")) - - query = query.Where("job.job_state != ?").Where("((job.start_time BETWEEN ? AND ?) OR (job.start_time + job.duration) BETWEEN ? AND ? OR (job.start_time < ?) AND (job.start_time + job.duration) > ?)", - "running", startTimeTail, stopTimeTail, startTimeFront, stopTimeTail, startTime, stopTime) - query = query.Where("job.resources LIKE ?", fmt.Sprint("%", hostname, "%")) - - rows, err := query.RunWith(r.stmtCache).Query() - if err != nil { - log.Errorf("Error while running query: %v", err) - return nil, err - } - - items := make([]*model.JobLink, 0, 10) - queryString := fmt.Sprintf("cluster=%s", job.Cluster) - - for rows.Next() { - var id, jobId, startTime sql.NullInt64 - - if err = rows.Scan(&id, &jobId, &startTime); err != nil { - log.Warn("Error while scanning rows") - return nil, err - } - - if id.Valid { - queryString += fmt.Sprintf("&jobId=%d", int(jobId.Int64)) - items = append(items, - &model.JobLink{ - ID: fmt.Sprint(id.Int64), - JobID: int(jobId.Int64), - }) - } - } - - rows, err = queryRunning.RunWith(r.stmtCache).Query() - if err != nil { - log.Errorf("Error while running query: %v", err) - return nil, err - } - - for rows.Next() { - var id, jobId, startTime sql.NullInt64 - - if err := rows.Scan(&id, &jobId, &startTime); err != nil { - log.Warn("Error while scanning rows") - return nil, err - } - - if id.Valid { - queryString += fmt.Sprintf("&jobId=%d", int(jobId.Int64)) - items = append(items, - &model.JobLink{ - ID: fmt.Sprint(id.Int64), - JobID: int(jobId.Int64), - }) - } - } - - cnt := len(items) - - return &model.JobLinkResultList{ - ListQuery: &queryString, - Items: items, - Count: &cnt, - }, nil -} - -// Start inserts a new job in the table, returning the unique job ID. -// Statistics are not transfered! -func (r *JobRepository) Start(job *schema.JobMeta) (id int64, err error) { - job.RawFootprint, err = json.Marshal(job.Footprint) - if err != nil { - return -1, fmt.Errorf("REPOSITORY/JOB > encoding footprint field failed: %w", err) - } - - job.RawResources, err = json.Marshal(job.Resources) - if err != nil { - return -1, fmt.Errorf("REPOSITORY/JOB > encoding resources field failed: %w", err) - } - - job.RawMetaData, err = json.Marshal(job.MetaData) - if err != nil { - return -1, fmt.Errorf("REPOSITORY/JOB > encoding metaData field failed: %w", err) - } - - res, err := r.DB.NamedExec(`INSERT INTO job ( - job_id, user, project, cluster, subcluster, `+"`partition`"+`, array_job_id, num_nodes, num_hwthreads, num_acc, - exclusive, monitoring_status, smt, job_state, start_time, duration, walltime, footprint, resources, meta_data - ) VALUES ( - :job_id, :user, :project, :cluster, :subcluster, :partition, :array_job_id, :num_nodes, :num_hwthreads, :num_acc, - :exclusive, :monitoring_status, :smt, :job_state, :start_time, :duration, :walltime, :footprint, :resources, :meta_data - );`, job) - if err != nil { - return -1, err - } - - return res.LastInsertId() -} - -// Stop updates the job with the database id jobId using the provided arguments. -func (r *JobRepository) Stop( - jobId int64, - duration int32, - state schema.JobState, - monitoringStatus int32, -) (err error) { - stmt := sq.Update("job"). - Set("job_state", state). - Set("duration", duration). - Set("monitoring_status", monitoringStatus). - Where("job.id = ?", jobId) - - _, err = stmt.RunWith(r.stmtCache).Exec() - return -} - func (r *JobRepository) DeleteJobsBefore(startTime int64) (int, error) { var cnt int q := sq.Select("count(*)").From("job").Where("job.start_time < ?", startTime) @@ -789,26 +565,3 @@ func (r *JobRepository) FindJobsBetween(startTimeBegin int64, startTimeEnd int64 log.Infof("Return job count %d", len(jobs)) return jobs, nil } - -const NamedJobInsert string = `INSERT INTO job ( - job_id, user, project, cluster, subcluster, ` + "`partition`" + `, array_job_id, num_nodes, num_hwthreads, num_acc, - exclusive, monitoring_status, smt, job_state, start_time, duration, walltime, footprint, resources, meta_data -) VALUES ( - :job_id, :user, :project, :cluster, :subcluster, :partition, :array_job_id, :num_nodes, :num_hwthreads, :num_acc, - :exclusive, :monitoring_status, :smt, :job_state, :start_time, :duration, :walltime, :footprint, :resources, :meta_data -);` - -func (r *JobRepository) InsertJob(job *schema.Job) (int64, error) { - res, err := r.DB.NamedExec(NamedJobInsert, job) - if err != nil { - log.Warn("Error while NamedJobInsert") - return 0, err - } - id, err := res.LastInsertId() - if err != nil { - log.Warn("Error while getting last insert ID") - return 0, err - } - - return id, nil -} diff --git a/internal/repository/jobCreate.go b/internal/repository/jobCreate.go new file mode 100644 index 0000000..43c26c1 --- /dev/null +++ b/internal/repository/jobCreate.go @@ -0,0 +1,75 @@ +// Copyright (C) NHR@FAU, University Erlangen-Nuremberg. +// All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package repository + +import ( + "encoding/json" + "fmt" + + "github.com/ClusterCockpit/cc-backend/pkg/log" + "github.com/ClusterCockpit/cc-backend/pkg/schema" + sq "github.com/Masterminds/squirrel" +) + +const NamedJobInsert string = `INSERT INTO job ( + job_id, user, project, cluster, subcluster, ` + "`partition`" + `, array_job_id, num_nodes, num_hwthreads, num_acc, + exclusive, monitoring_status, smt, job_state, start_time, duration, walltime, footprint, resources, meta_data +) VALUES ( + :job_id, :user, :project, :cluster, :subcluster, :partition, :array_job_id, :num_nodes, :num_hwthreads, :num_acc, + :exclusive, :monitoring_status, :smt, :job_state, :start_time, :duration, :walltime, :footprint, :resources, :meta_data +);` + +func (r *JobRepository) InsertJob(job *schema.JobMeta) (int64, error) { + res, err := r.DB.NamedExec(NamedJobInsert, job) + if err != nil { + log.Warn("Error while NamedJobInsert") + return 0, err + } + id, err := res.LastInsertId() + if err != nil { + log.Warn("Error while getting last insert ID") + return 0, err + } + + return id, nil +} + +// Start inserts a new job in the table, returning the unique job ID. +// Statistics are not transfered! +func (r *JobRepository) Start(job *schema.JobMeta) (id int64, err error) { + job.RawFootprint, err = json.Marshal(job.Footprint) + if err != nil { + return -1, fmt.Errorf("REPOSITORY/JOB > encoding footprint field failed: %w", err) + } + + job.RawResources, err = json.Marshal(job.Resources) + if err != nil { + return -1, fmt.Errorf("REPOSITORY/JOB > encoding resources field failed: %w", err) + } + + job.RawMetaData, err = json.Marshal(job.MetaData) + if err != nil { + return -1, fmt.Errorf("REPOSITORY/JOB > encoding metaData field failed: %w", err) + } + + return r.InsertJob(job) +} + +// Stop updates the job with the database id jobId using the provided arguments. +func (r *JobRepository) Stop( + jobId int64, + duration int32, + state schema.JobState, + monitoringStatus int32, +) (err error) { + stmt := sq.Update("job"). + Set("job_state", state). + Set("duration", duration). + Set("monitoring_status", monitoringStatus). + Where("job.id = ?", jobId) + + _, err = stmt.RunWith(r.stmtCache).Exec() + return +} diff --git a/internal/repository/jobFind.go b/internal/repository/jobFind.go new file mode 100644 index 0000000..f1264dd --- /dev/null +++ b/internal/repository/jobFind.go @@ -0,0 +1,192 @@ +// Copyright (C) NHR@FAU, University Erlangen-Nuremberg. +// All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package repository + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/ClusterCockpit/cc-backend/internal/graph/model" + "github.com/ClusterCockpit/cc-backend/pkg/log" + "github.com/ClusterCockpit/cc-backend/pkg/schema" + sq "github.com/Masterminds/squirrel" +) + +// Find executes a SQL query to find a specific batch job. +// The job is queried using the batch job id, the cluster name, +// and the start time of the job in UNIX epoch time seconds. +// It returns a pointer to a schema.Job data structure and an error variable. +// To check if no job was found test err == sql.ErrNoRows +func (r *JobRepository) Find( + jobId *int64, + cluster *string, + startTime *int64, +) (*schema.Job, error) { + start := time.Now() + q := sq.Select(jobColumns...).From("job"). + Where("job.job_id = ?", *jobId) + + if cluster != nil { + q = q.Where("job.cluster = ?", *cluster) + } + if startTime != nil { + q = q.Where("job.start_time = ?", *startTime) + } + + log.Debugf("Timer Find %s", time.Since(start)) + + return scanJob(q.RunWith(r.stmtCache).QueryRow()) +} + +// Find executes a SQL query to find a specific batch job. +// The job is queried using the batch job id, the cluster name, +// and the start time of the job in UNIX epoch time seconds. +// It returns a pointer to a schema.Job data structure and an error variable. +// To check if no job was found test err == sql.ErrNoRows +func (r *JobRepository) FindAll( + jobId *int64, + cluster *string, + startTime *int64, +) ([]*schema.Job, error) { + start := time.Now() + q := sq.Select(jobColumns...).From("job"). + Where("job.job_id = ?", *jobId) + + if cluster != nil { + q = q.Where("job.cluster = ?", *cluster) + } + if startTime != nil { + q = q.Where("job.start_time = ?", *startTime) + } + + rows, err := q.RunWith(r.stmtCache).Query() + if err != nil { + log.Error("Error while running query") + return nil, err + } + + jobs := make([]*schema.Job, 0, 10) + for rows.Next() { + job, err := scanJob(rows) + if err != nil { + log.Warn("Error while scanning rows") + return nil, err + } + jobs = append(jobs, job) + } + log.Debugf("Timer FindAll %s", time.Since(start)) + return jobs, nil +} + +// FindById executes a SQL query to find a specific batch job. +// The job is queried using the database id. +// It returns a pointer to a schema.Job data structure and an error variable. +// To check if no job was found test err == sql.ErrNoRows +func (r *JobRepository) FindById(jobId int64) (*schema.Job, error) { + q := sq.Select(jobColumns...). + From("job").Where("job.id = ?", jobId) + return scanJob(q.RunWith(r.stmtCache).QueryRow()) +} + +func (r *JobRepository) FindConcurrentJobs( + ctx context.Context, + job *schema.Job, +) (*model.JobLinkResultList, error) { + if job == nil { + return nil, nil + } + + query, qerr := SecurityCheck(ctx, sq.Select("job.id", "job.job_id", "job.start_time").From("job")) + if qerr != nil { + return nil, qerr + } + + query = query.Where("cluster = ?", job.Cluster) + var startTime int64 + var stopTime int64 + + startTime = job.StartTimeUnix + hostname := job.Resources[0].Hostname + + if job.State == schema.JobStateRunning { + stopTime = time.Now().Unix() + } else { + stopTime = startTime + int64(job.Duration) + } + + // Add 200s overlap for jobs start time at the end + startTimeTail := startTime + 10 + stopTimeTail := stopTime - 200 + startTimeFront := startTime + 200 + + queryRunning := query.Where("job.job_state = ?").Where("(job.start_time BETWEEN ? AND ? OR job.start_time < ?)", + "running", startTimeTail, stopTimeTail, startTime) + queryRunning = queryRunning.Where("job.resources LIKE ?", fmt.Sprint("%", hostname, "%")) + + query = query.Where("job.job_state != ?").Where("((job.start_time BETWEEN ? AND ?) OR (job.start_time + job.duration) BETWEEN ? AND ? OR (job.start_time < ?) AND (job.start_time + job.duration) > ?)", + "running", startTimeTail, stopTimeTail, startTimeFront, stopTimeTail, startTime, stopTime) + query = query.Where("job.resources LIKE ?", fmt.Sprint("%", hostname, "%")) + + rows, err := query.RunWith(r.stmtCache).Query() + if err != nil { + log.Errorf("Error while running query: %v", err) + return nil, err + } + + items := make([]*model.JobLink, 0, 10) + queryString := fmt.Sprintf("cluster=%s", job.Cluster) + + for rows.Next() { + var id, jobId, startTime sql.NullInt64 + + if err = rows.Scan(&id, &jobId, &startTime); err != nil { + log.Warn("Error while scanning rows") + return nil, err + } + + if id.Valid { + queryString += fmt.Sprintf("&jobId=%d", int(jobId.Int64)) + items = append(items, + &model.JobLink{ + ID: fmt.Sprint(id.Int64), + JobID: int(jobId.Int64), + }) + } + } + + rows, err = queryRunning.RunWith(r.stmtCache).Query() + if err != nil { + log.Errorf("Error while running query: %v", err) + return nil, err + } + + for rows.Next() { + var id, jobId, startTime sql.NullInt64 + + if err := rows.Scan(&id, &jobId, &startTime); err != nil { + log.Warn("Error while scanning rows") + return nil, err + } + + if id.Valid { + queryString += fmt.Sprintf("&jobId=%d", int(jobId.Int64)) + items = append(items, + &model.JobLink{ + ID: fmt.Sprint(id.Int64), + JobID: int(jobId.Int64), + }) + } + } + + cnt := len(items) + + return &model.JobLinkResultList{ + ListQuery: &queryString, + Items: items, + Count: &cnt, + }, nil +} diff --git a/internal/repository/query.go b/internal/repository/jobQuery.go similarity index 100% rename from internal/repository/query.go rename to internal/repository/jobQuery.go diff --git a/internal/repository/user.go b/internal/repository/user.go index 3b7d945..a8776a9 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -72,7 +72,6 @@ func (r *UserRepository) GetUser(username string) (*schema.User, error) { } func (r *UserRepository) GetLdapUsernames() ([]string, error) { - var users []string rows, err := r.DB.Query(`SELECT username FROM user WHERE user.ldap = 1`) if err != nil { @@ -132,7 +131,6 @@ func (r *UserRepository) AddUser(user *schema.User) error { } func (r *UserRepository) DelUser(username string) error { - _, err := r.DB.Exec(`DELETE FROM user WHERE user.username = ?`, username) if err != nil { log.Errorf("Error while deleting user '%s' from DB", username) @@ -143,7 +141,6 @@ func (r *UserRepository) DelUser(username string) error { } func (r *UserRepository) ListUsers(specialsOnly bool) ([]*schema.User, error) { - q := sq.Select("username", "name", "email", "roles", "projects").From("user") if specialsOnly { q = q.Where("(roles != '[\"user\"]' AND roles != '[]')") @@ -186,8 +183,8 @@ func (r *UserRepository) ListUsers(specialsOnly bool) ([]*schema.User, error) { func (r *UserRepository) AddRole( ctx context.Context, username string, - queryrole string) error { - + queryrole string, +) error { newRole := strings.ToLower(queryrole) user, err := r.GetUser(username) if err != nil { @@ -198,15 +195,15 @@ func (r *UserRepository) AddRole( exists, valid := user.HasValidRole(newRole) if !valid { - return fmt.Errorf("Supplied role is no valid option : %v", newRole) + return fmt.Errorf("supplied role is no valid option : %v", newRole) } if exists { - return fmt.Errorf("User %v already has role %v", username, newRole) + return fmt.Errorf("user %v already has role %v", username, newRole) } roles, _ := json.Marshal(append(user.Roles, newRole)) if _, err := sq.Update("user").Set("roles", roles).Where("user.username = ?", username).RunWith(r.DB).Exec(); err != nil { - log.Errorf("Error while adding new role for user '%s'", user.Username) + log.Errorf("error while adding new role for user '%s'", user.Username) return err } return nil @@ -223,14 +220,14 @@ func (r *UserRepository) RemoveRole(ctx context.Context, username string, queryr exists, valid := user.HasValidRole(oldRole) if !valid { - return fmt.Errorf("Supplied role is no valid option : %v", oldRole) + return fmt.Errorf("supplied role is no valid option : %v", oldRole) } if !exists { - return fmt.Errorf("Role already deleted for user '%v': %v", username, oldRole) + return fmt.Errorf("role already deleted for user '%v': %v", username, oldRole) } if oldRole == schema.GetRoleString(schema.RoleManager) && len(user.Projects) != 0 { - return fmt.Errorf("Cannot remove role 'manager' while user %s still has assigned project(s) : %v", username, user.Projects) + return fmt.Errorf("cannot remove role 'manager' while user %s still has assigned project(s) : %v", username, user.Projects) } var newroles []string @@ -240,7 +237,7 @@ func (r *UserRepository) RemoveRole(ctx context.Context, username string, queryr } } - var mroles, _ = json.Marshal(newroles) + mroles, _ := json.Marshal(newroles) if _, err := sq.Update("user").Set("roles", mroles).Where("user.username = ?", username).RunWith(r.DB).Exec(); err != nil { log.Errorf("Error while removing role for user '%s'", user.Username) return err @@ -251,15 +248,15 @@ func (r *UserRepository) RemoveRole(ctx context.Context, username string, queryr func (r *UserRepository) AddProject( ctx context.Context, username string, - project string) error { - + project string, +) error { user, err := r.GetUser(username) if err != nil { return err } if !user.HasRole(schema.RoleManager) { - return fmt.Errorf("user '%s' is not a manager!", username) + return fmt.Errorf("user '%s' is not a manager", username) } if user.HasProject(project) { @@ -281,11 +278,11 @@ func (r *UserRepository) RemoveProject(ctx context.Context, username string, pro } if !user.HasRole(schema.RoleManager) { - return fmt.Errorf("user '%#v' is not a manager!", username) + return fmt.Errorf("user '%#v' is not a manager", username) } if !user.HasProject(project) { - return fmt.Errorf("user '%#v': Cannot remove project '%#v' - Does not match!", username, project) + return fmt.Errorf("user '%#v': Cannot remove project '%#v' - Does not match", username, project) } var exists bool @@ -298,7 +295,7 @@ func (r *UserRepository) RemoveProject(ctx context.Context, username string, pro } } - if exists == true { + if exists { var result interface{} if len(newprojects) == 0 { result = "[]"