github.com/nais/knorten@v0.0.0-20240104110906-55926958e361/pkg/api/admin.go (about)

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