github.com/navikt/knorten@v0.0.0-20240419132333-1333f46ed8b6/pkg/api/admin.go (about)

     1  package api
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"encoding/gob"
     7  	"errors"
     8  	"fmt"
     9  	"net/http"
    10  	"net/url"
    11  	"strings"
    12  
    13  	"github.com/navikt/knorten/pkg/api/middlewares"
    14  
    15  	"github.com/google/uuid"
    16  	"github.com/navikt/knorten/pkg/chart"
    17  	"github.com/navikt/knorten/pkg/database"
    18  	"github.com/navikt/knorten/pkg/database/gensql"
    19  	"github.com/navikt/knorten/pkg/k8s"
    20  
    21  	"github.com/gin-contrib/sessions"
    22  	"github.com/gin-gonic/gin"
    23  )
    24  
    25  type diffValue struct {
    26  	Old       string
    27  	New       string
    28  	Encrypted string
    29  }
    30  
    31  type teamInfo struct {
    32  	gensql.Team
    33  	Namespace string
    34  	Apps      []gensql.ChartType
    35  	Events    []gensql.Event
    36  }
    37  
    38  func (c *client) setupAdminRoutes() {
    39  	c.router.GET("/admin", func(ctx *gin.Context) {
    40  		session := sessions.Default(ctx)
    41  		flashes := session.Flashes()
    42  		err := session.Save()
    43  		if err != nil {
    44  			c.log.WithError(err).Error("problem saving session")
    45  			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err})
    46  			return
    47  		}
    48  
    49  		teams, err := c.repo.TeamsGet(ctx)
    50  		if err != nil {
    51  			session := sessions.Default(ctx)
    52  			session.AddFlash(err.Error())
    53  			err = session.Save()
    54  			if err != nil {
    55  				c.log.WithError(err).Error("problem saving session")
    56  				ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err})
    57  				return
    58  			}
    59  
    60  			ctx.Redirect(http.StatusSeeOther, "/admin")
    61  			return
    62  		}
    63  
    64  		teamApps := map[string]teamInfo{}
    65  		for _, team := range teams {
    66  			apps, err := c.repo.ChartsForTeamGet(ctx, team.ID)
    67  			if err != nil {
    68  				c.log.WithError(err).Error("problem retrieving apps for teams")
    69  				ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err})
    70  				return
    71  			}
    72  			events, err := c.repo.EventsByOwnerGet(ctx, team.ID, 5)
    73  			if err != nil {
    74  				c.log.WithError(err).Error("problem retrieving apps for teams")
    75  				ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err})
    76  				return
    77  			}
    78  
    79  			teamApps[team.ID] = teamInfo{
    80  				Team:      team,
    81  				Namespace: k8s.TeamIDToNamespace(team.ID),
    82  				Apps:      apps,
    83  				Events:    events,
    84  			}
    85  		}
    86  
    87  		ctx.HTML(http.StatusOK, "admin/index", gin.H{
    88  			"errors":     flashes,
    89  			"teams":      teamApps,
    90  			"gcpProject": c.gcpProject,
    91  			"loggedIn":   ctx.GetBool(middlewares.LoggedInKey),
    92  			"isAdmin":    ctx.GetBool(middlewares.AdminKey),
    93  		})
    94  	})
    95  
    96  	c.router.GET("/admin/:chart", func(ctx *gin.Context) {
    97  		chartType := getChartType(ctx.Param("chart"))
    98  
    99  		values, err := c.repo.GlobalValuesGet(ctx, chartType)
   100  		if err != nil {
   101  			session := sessions.Default(ctx)
   102  			session.AddFlash(err.Error())
   103  			err = session.Save()
   104  			if err != nil {
   105  				c.log.WithError(err).Error("problem saving session")
   106  				ctx.Redirect(http.StatusSeeOther, "/admin")
   107  				return
   108  			}
   109  
   110  			ctx.Redirect(http.StatusSeeOther, "/admin")
   111  			return
   112  		}
   113  
   114  		session := sessions.Default(ctx)
   115  		flashes := session.Flashes()
   116  		err = session.Save()
   117  		if err != nil {
   118  			c.log.WithError(err).Error("problem saving session")
   119  			ctx.Redirect(http.StatusSeeOther, "/admin")
   120  			return
   121  		}
   122  
   123  		ctx.HTML(http.StatusOK, "admin/chart", gin.H{
   124  			"values":   values,
   125  			"errors":   flashes,
   126  			"chart":    string(chartType),
   127  			"loggedIn": ctx.GetBool(middlewares.LoggedInKey),
   128  			"isAdmin":  ctx.GetBool(middlewares.AdminKey),
   129  		})
   130  	})
   131  
   132  	c.router.POST("/admin/:chart", func(ctx *gin.Context) {
   133  		session := sessions.Default(ctx)
   134  		chartType := getChartType(ctx.Param("chart"))
   135  
   136  		err := ctx.Request.ParseForm()
   137  		if err != nil {
   138  			session.AddFlash(err.Error())
   139  			err = session.Save()
   140  			if err != nil {
   141  				c.log.WithError(err).Error("problem saving session")
   142  				ctx.Redirect(http.StatusSeeOther, "admin")
   143  				return
   144  			}
   145  			ctx.Redirect(http.StatusSeeOther, "admin")
   146  			return
   147  		}
   148  
   149  		changedValues, err := c.findGlobalValueChanges(ctx, ctx.Request.PostForm, chartType)
   150  		if err != nil {
   151  			session := sessions.Default(ctx)
   152  			session.AddFlash(err.Error())
   153  			err = session.Save()
   154  			if err != nil {
   155  				c.log.WithError(err).Error("problem saving session")
   156  				ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v", chartType))
   157  				return
   158  			}
   159  
   160  			ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v", chartType))
   161  			return
   162  		}
   163  
   164  		if len(changedValues) == 0 {
   165  			session.AddFlash("Ingen endringer lagret")
   166  			err = session.Save()
   167  			if err != nil {
   168  				c.log.WithError(err).Error("problem saving session")
   169  				ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v", chartType))
   170  				return
   171  			}
   172  			ctx.Redirect(http.StatusSeeOther, "/admin")
   173  			return
   174  		}
   175  
   176  		gob.Register(changedValues)
   177  		session.AddFlash(changedValues)
   178  		err = session.Save()
   179  		if err != nil {
   180  			c.log.WithError(err).Error("problem saving session")
   181  			ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v", chartType))
   182  			return
   183  		}
   184  
   185  		ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v/confirm", chartType))
   186  	})
   187  
   188  	c.router.GET("/admin/:chart/confirm", func(ctx *gin.Context) {
   189  		chartType := getChartType(ctx.Param("chart"))
   190  		session := sessions.Default(ctx)
   191  		changedValues := session.Flashes()
   192  		err := session.Save()
   193  		if err != nil {
   194  			c.log.WithError(err).Error("problem saving session")
   195  			ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v", chartType))
   196  			return
   197  		}
   198  
   199  		ctx.HTML(http.StatusOK, "admin/confirm", gin.H{
   200  			"changedValues": changedValues,
   201  			"chart":         string(chartType),
   202  			"loggedIn":      ctx.GetBool(middlewares.LoggedInKey),
   203  			"isAdmin":       ctx.GetBool(middlewares.AdminKey),
   204  		})
   205  	})
   206  
   207  	c.router.POST("/admin/:chart/confirm", func(ctx *gin.Context) {
   208  		session := sessions.Default(ctx)
   209  		chartType := getChartType(ctx.Param("chart"))
   210  
   211  		err := ctx.Request.ParseForm()
   212  		if err != nil {
   213  			c.log.WithError(err)
   214  			session.AddFlash(err.Error())
   215  			err = session.Save()
   216  			if err != nil {
   217  				c.log.WithError(err).Error("problem saving session")
   218  				ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v/confirm", chartType))
   219  				return
   220  			}
   221  			ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v/confirm", chartType))
   222  			return
   223  		}
   224  
   225  		if err := c.updateGlobalValues(ctx, ctx.Request.PostForm, chartType); err != nil {
   226  			c.log.WithError(err)
   227  			session.AddFlash(err.Error())
   228  			err = session.Save()
   229  			if err != nil {
   230  				c.log.WithError(err).Error("problem saving session")
   231  				ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v", chartType))
   232  				return
   233  			}
   234  			ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v", chartType))
   235  			return
   236  		}
   237  
   238  		if err != nil {
   239  			c.log.WithError(err)
   240  			session.AddFlash(err.Error())
   241  			err = session.Save()
   242  			if err != nil {
   243  				c.log.WithError(err).Error("problem saving session")
   244  				ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v/confirm", chartType))
   245  				return
   246  			}
   247  			ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v/confirm", chartType))
   248  			return
   249  		}
   250  
   251  		ctx.Redirect(http.StatusSeeOther, "/admin")
   252  	})
   253  
   254  	c.router.POST("/admin/:chart/sync", func(ctx *gin.Context) {
   255  		session := sessions.Default(ctx)
   256  		chartType := getChartType(ctx.Param("chart"))
   257  		team := ctx.PostForm("team")
   258  
   259  		if err := c.syncChart(ctx, team, chartType); err != nil {
   260  			c.log.WithError(err).Errorf("syncing %v", chartType)
   261  			session.AddFlash(err.Error())
   262  			err = session.Save()
   263  			if err != nil {
   264  				c.log.WithError(err).Error("problem saving session")
   265  			}
   266  		}
   267  
   268  		ctx.Redirect(http.StatusSeeOther, "/admin")
   269  	})
   270  
   271  	c.router.POST("/admin/:chart/sync/all", func(ctx *gin.Context) {
   272  		session := sessions.Default(ctx)
   273  		chartType := getChartType(ctx.Param("chart"))
   274  
   275  		if err := c.syncChartForAllTeams(ctx, chartType); err != nil {
   276  			c.log.WithError(err).Errorf("resyncing all instances of %v", chartType)
   277  			session.AddFlash(err.Error())
   278  			err = session.Save()
   279  			if err != nil {
   280  				c.log.WithError(err).Error("problem saving session")
   281  			}
   282  		}
   283  
   284  		ctx.Redirect(http.StatusSeeOther, "/admin")
   285  	})
   286  
   287  	c.router.POST("/admin/team/sync/all", func(ctx *gin.Context) {
   288  		session := sessions.Default(ctx)
   289  
   290  		if err := c.syncTeams(ctx); err != nil {
   291  			c.log.WithError(err).Errorf("resyncing all teams")
   292  			session.AddFlash(err.Error())
   293  			err = session.Save()
   294  			if err != nil {
   295  				c.log.WithError(err).Error("problem saving session")
   296  			}
   297  		}
   298  
   299  		ctx.Redirect(http.StatusSeeOther, "/admin")
   300  	})
   301  
   302  	c.router.POST("/admin/team/:team/delete", func(ctx *gin.Context) {
   303  		session := sessions.Default(ctx)
   304  		slug := ctx.Param("team")
   305  
   306  		team, err := c.repo.TeamBySlugGet(ctx, slug)
   307  		if err != nil {
   308  			c.log.WithError(err).Errorf("deleting team")
   309  			session.AddFlash(err.Error())
   310  			err = session.Save()
   311  			if err != nil {
   312  				c.log.WithError(err).Error("problem saving session")
   313  			}
   314  		}
   315  
   316  		if err := c.repo.RegisterDeleteTeamEvent(ctx, team.ID); err != nil {
   317  			c.log.WithError(err).Errorf("registering delete team event")
   318  			session.AddFlash(err.Error())
   319  			err = session.Save()
   320  			if err != nil {
   321  				c.log.WithError(err).Error("problem saving session")
   322  			}
   323  		}
   324  
   325  		ctx.Redirect(http.StatusSeeOther, "/admin")
   326  	})
   327  
   328  	c.router.GET("/admin/event/:id", func(ctx *gin.Context) {
   329  		header, err := c.getEvent(ctx)
   330  		if err != nil {
   331  			c.log.WithError(err).Errorf("getting event logs")
   332  			session := sessions.Default(ctx)
   333  			session.AddFlash(err.Error())
   334  			err = session.Save()
   335  			if err != nil {
   336  				c.log.WithError(err).Error("problem saving session")
   337  			}
   338  			ctx.Redirect(http.StatusSeeOther, "/admin")
   339  		}
   340  
   341  		header["loggedIn"] = ctx.GetBool(middlewares.LoggedInKey)
   342  		header["isAdmin"] = ctx.GetBool(middlewares.AdminKey)
   343  
   344  		ctx.HTML(http.StatusOK, "admin/event", header)
   345  	})
   346  
   347  	c.router.POST("/admin/event/:id", func(ctx *gin.Context) {
   348  		err := c.setEventStatus(ctx)
   349  		if err != nil {
   350  			c.log.WithError(err).Errorf("setting event status")
   351  			session := sessions.Default(ctx)
   352  			session.AddFlash(err.Error())
   353  			err = session.Save()
   354  			if err != nil {
   355  				c.log.WithError(err).Error("problem saving session")
   356  			}
   357  			ctx.Redirect(http.StatusSeeOther, "/admin/event/"+ctx.Param("id"))
   358  		}
   359  
   360  		ctx.Redirect(http.StatusSeeOther, "/admin/event/"+ctx.Param("id"))
   361  	})
   362  }
   363  
   364  func (c *client) syncTeams(ctx context.Context) error {
   365  	teams, err := c.repo.TeamsGet(ctx)
   366  	if err != nil {
   367  		return err
   368  	}
   369  
   370  	for _, team := range teams {
   371  		err := c.repo.RegisterUpdateTeamEvent(ctx, team)
   372  		if err != nil {
   373  			return err
   374  		}
   375  	}
   376  
   377  	return nil
   378  }
   379  
   380  func (c *client) syncChartForAllTeams(ctx context.Context, chartType gensql.ChartType) error {
   381  	teams, err := c.repo.TeamsForChartGet(ctx, chartType)
   382  	if err != nil {
   383  		return err
   384  	}
   385  
   386  	for _, teamID := range teams {
   387  		err := c.syncChart(ctx, teamID, chartType)
   388  		if err != nil {
   389  			return err
   390  		}
   391  	}
   392  
   393  	return nil
   394  }
   395  
   396  func (c *client) syncChart(ctx context.Context, teamID string, chartType gensql.ChartType) error {
   397  	switch chartType {
   398  	case gensql.ChartTypeJupyterhub:
   399  		pypiAccessValue, err := c.repo.TeamValueGet(ctx, chart.TeamValueKeyPYPIAccess, teamID)
   400  		if err != nil && !errors.Is(err, sql.ErrNoRows) {
   401  			return err
   402  		}
   403  		values := chart.JupyterConfigurableValues{
   404  			TeamID:     teamID,
   405  			PYPIAccess: pypiAccessValue.Value == "true",
   406  		}
   407  		return c.repo.RegisterUpdateJupyterEvent(ctx, teamID, values)
   408  	case gensql.ChartTypeAirflow:
   409  		values := chart.AirflowConfigurableValues{
   410  			TeamID: teamID,
   411  		}
   412  		return c.repo.RegisterUpdateAirflowEvent(ctx, teamID, values)
   413  	}
   414  
   415  	return nil
   416  }
   417  
   418  func (c *client) findGlobalValueChanges(ctx context.Context, formValues url.Values, chartType gensql.ChartType) (map[string]diffValue, error) {
   419  	originals, err := c.repo.GlobalValuesGet(ctx, chartType)
   420  	if err != nil {
   421  		return nil, err
   422  	}
   423  
   424  	changed := findChangedValues(originals, formValues)
   425  	findDeletedValues(changed, originals, formValues)
   426  
   427  	return changed, nil
   428  }
   429  
   430  func (c *client) updateGlobalValues(ctx context.Context, formValues url.Values, chartType gensql.ChartType) error {
   431  	for key, values := range formValues {
   432  		if values[0] == "" {
   433  			err := c.repo.GlobalValueDelete(ctx, key, chartType)
   434  			if err != nil {
   435  				return err
   436  			}
   437  		} else {
   438  			value, encrypted, err := c.parseValue(values)
   439  			if err != nil {
   440  				return err
   441  			}
   442  
   443  			err = c.repo.GlobalChartValueInsert(ctx, key, value, encrypted, chartType)
   444  			if err != nil {
   445  				return err
   446  			}
   447  		}
   448  	}
   449  
   450  	return c.syncChartForAllTeams(ctx, chartType)
   451  }
   452  
   453  func (c *client) parseValue(values []string) (string, bool, error) {
   454  	if len(values) == 2 {
   455  		value, err := c.repo.EncryptValue(values[0])
   456  		if err != nil {
   457  			return "", false, err
   458  		}
   459  		return value, true, nil
   460  	}
   461  
   462  	return values[0], false, nil
   463  }
   464  
   465  func findDeletedValues(changedValues map[string]diffValue, originals []gensql.ChartGlobalValue, formValues url.Values) {
   466  	for _, original := range originals {
   467  		notFound := true
   468  		for key := range formValues {
   469  			if original.Key == key {
   470  				notFound = false
   471  				break
   472  			}
   473  		}
   474  
   475  		if notFound {
   476  			changedValues[original.Key] = diffValue{
   477  				Old: original.Value,
   478  			}
   479  		}
   480  	}
   481  }
   482  
   483  func findChangedValues(originals []gensql.ChartGlobalValue, formValues url.Values) map[string]diffValue {
   484  	changedValues := map[string]diffValue{}
   485  
   486  	for key, values := range formValues {
   487  		var encrypted string
   488  		value := values[0]
   489  		if len(values) == 2 {
   490  			encrypted = values[1]
   491  		}
   492  
   493  		if strings.HasPrefix(key, "key") {
   494  			correctValue := valueForKey(changedValues, key)
   495  			if correctValue != nil {
   496  				changedValues[value] = *correctValue
   497  				delete(changedValues, key)
   498  			} else {
   499  				key := strings.Replace(key, "key", "value", 1)
   500  				diff := diffValue{
   501  					New:       key,
   502  					Encrypted: encrypted,
   503  				}
   504  				changedValues[value] = diff
   505  			}
   506  		} else if strings.HasPrefix(key, "value") {
   507  			correctKey := keyForValue(changedValues, key)
   508  			if correctKey != "" {
   509  				diff := diffValue{
   510  					New:       value,
   511  					Encrypted: encrypted,
   512  				}
   513  				changedValues[correctKey] = diff
   514  			} else {
   515  				key := strings.Replace(key, "value", "key", 1)
   516  				diff := diffValue{
   517  					New:       value,
   518  					Encrypted: encrypted,
   519  				}
   520  				changedValues[key] = diff
   521  			}
   522  		} else {
   523  			for _, originalValue := range originals {
   524  				if originalValue.Key == key {
   525  					if originalValue.Value != value {
   526  						diff := diffValue{
   527  							Old:       originalValue.Value,
   528  							New:       value,
   529  							Encrypted: encrypted,
   530  						}
   531  						changedValues[key] = diff
   532  						break
   533  					}
   534  				}
   535  			}
   536  		}
   537  	}
   538  
   539  	return changedValues
   540  }
   541  
   542  func valueForKey(values map[string]diffValue, needle string) *diffValue {
   543  	for key, value := range values {
   544  		if key == needle {
   545  			return &value
   546  		}
   547  	}
   548  
   549  	return nil
   550  }
   551  
   552  func keyForValue(values map[string]diffValue, needle string) string {
   553  	for key, value := range values {
   554  		if value.New == needle {
   555  			return key
   556  		}
   557  	}
   558  
   559  	return ""
   560  }
   561  
   562  func (c *client) getEvent(ctx *gin.Context) (gin.H, error) {
   563  	eventID, err := uuid.Parse(ctx.Param("id"))
   564  	if err != nil {
   565  		return gin.H{}, err
   566  	}
   567  
   568  	event, err := c.repo.EventGet(ctx, eventID)
   569  	if err != nil {
   570  		return gin.H{}, err
   571  	}
   572  
   573  	eventLogs, err := c.repo.EventLogsForEventGet(ctx, eventID)
   574  	if err != nil {
   575  		return gin.H{}, err
   576  	}
   577  
   578  	return gin.H{
   579  		"event": event,
   580  		"logs":  eventLogs,
   581  	}, nil
   582  }
   583  
   584  func (c *client) setEventStatus(ctx *gin.Context) error {
   585  	eventID, err := uuid.Parse(ctx.Param("id"))
   586  	if err != nil {
   587  		return err
   588  	}
   589  
   590  	var status database.EventStatus
   591  	switch ctx.PostForm("status") {
   592  	case string(database.EventStatusNew):
   593  		status = database.EventStatusNew
   594  	case string(database.EventStatusManualFailed):
   595  		status = database.EventStatusManualFailed
   596  	default:
   597  		return fmt.Errorf("invalid status %v", ctx.PostForm("status"))
   598  	}
   599  
   600  	return c.repo.EventSetStatus(ctx, eventID, status)
   601  }