From fbed466c3d349619f99f8b9ff9a38f450c1e2235 Mon Sep 17 00:00:00 2001 From: David Allen Date: Sat, 30 Aug 2025 23:30:46 -0600 Subject: [PATCH] refactor: updated cmd and pkg implementations --- cmd/list.go | 86 ++-------------- cmd/plugins.go | 4 +- cmd/root.go | 31 ++++++ cmd/run.go | 6 +- cmd/upload.go | 218 +++++++++++++++++++++++++--------------- pkg/client/client.go | 41 ++++++++ pkg/service/plugins.go | 16 +-- pkg/service/profiles.go | 43 ++++++-- pkg/service/routes.go | 32 +++--- pkg/service/service.go | 6 +- 10 files changed, 287 insertions(+), 196 deletions(-) diff --git a/cmd/list.go b/cmd/list.go index 0de2797..1fa3fe6 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -81,10 +81,10 @@ var listPluginsCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { var ( host, _ = cmd.Flags().GetString("host") - path, _ = cmd.Flags().GetString("path") c = client.New(host) res *http.Response + query string plugins []string body []byte err error @@ -96,24 +96,7 @@ var listPluginsCmd = &cobra.Command{ Path: "/plugins", Method: http.MethodGet, }) - if err != nil { - log.Error().Err(err). - Str("host", host). - Str("path", path). - Msg("failed to make request") - os.Exit(1) - } - if res.StatusCode != http.StatusOK { - log.Error(). - Any("status", map[string]any{ - "code": res.StatusCode, - "message": res.Status, - }). - Str("host", host). - Str("path", path). - Msg("response returned bad status") - os.Exit(1) - } + handleResponseError(res, host, "/plugins", err) err = json.Unmarshal(body, &plugins) if err != nil { log.Error().Err(err). @@ -123,28 +106,12 @@ var listPluginsCmd = &cobra.Command{ } else { for _, pluginName := range args { // make request to /list endpoint + query = fmt.Sprintf("/plugins/%s/info", pluginName) res, body, err = c.MakeRequest(client.HTTPEnvelope{ - Path: fmt.Sprintf("/plugins/%s/info", pluginName), + Path: query, Method: http.MethodGet, }) - if err != nil { - log.Error().Err(err). - Str("host", host). - Str("path", path). - Msg("failed to make request") - os.Exit(1) - } - if res.StatusCode != http.StatusOK { - log.Error(). - Any("status", map[string]any{ - "code": res.StatusCode, - "message": res.Status, - }). - Str("host", host). - Str("path", path). - Msg("response returned bad status") - os.Exit(1) - } + handleResponseError(res, host, query, err) plugins = append(plugins, string(body)) } @@ -159,12 +126,12 @@ var listProfilesCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { var ( host, _ = cmd.Flags().GetString("host") - path, _ = cmd.Flags().GetString("path") c = client.New(host) res *http.Response profiles []makeshift.Profile body []byte + query string err error ) @@ -174,24 +141,7 @@ var listProfilesCmd = &cobra.Command{ Path: "/profiles", Method: http.MethodGet, }) - if err != nil { - log.Error().Err(err). - Str("host", host). - Str("path", path). - Msg("failed to make request") - os.Exit(1) - } - if res.StatusCode != http.StatusOK { - log.Error(). - Any("status", map[string]any{ - "code": res.StatusCode, - "message": res.Status, - }). - Str("host", host). - Str("path", path). - Msg("response returned bad status") - os.Exit(1) - } + handleResponseError(res, host, "/profiles", err) err = json.Unmarshal(body, &profiles) if err != nil { @@ -202,28 +152,12 @@ var listProfilesCmd = &cobra.Command{ } else { for _, profileID := range args { // make request to /list endpoint + query = fmt.Sprintf("/profiles/%s", profileID) res, body, err = c.MakeRequest(client.HTTPEnvelope{ - Path: fmt.Sprintf("/profiles/%s", profileID), + Path: fmt.Sprintf(query), Method: http.MethodGet, }) - if err != nil { - log.Error().Err(err). - Str("host", host). - Str("path", path). - Msg("failed to make request") - os.Exit(1) - } - if res.StatusCode != http.StatusOK { - log.Error(). - Any("status", map[string]any{ - "code": res.StatusCode, - "message": res.Status, - }). - Str("host", host). - Str("path", path). - Msg("response returned bad status") - os.Exit(1) - } + handleResponseError(res, host, query, err) var profile makeshift.Profile err = json.Unmarshal(body, &profile) if err != nil { diff --git a/cmd/plugins.go b/cmd/plugins.go index 812b5a8..8917b1c 100644 --- a/cmd/plugins.go +++ b/cmd/plugins.go @@ -178,8 +178,8 @@ var pluginsInfoCmd = &cobra.Command{ } func init() { - pluginsCompileCmd.PersistentFlags().StringP("output", "o", "", "Set the path to save compiled plugin") - pluginsInfoCmd.PersistentFlags().String("host", "http://localhost:5050", "Set the makeshift remote host (can be set with MAKESHIFT_HOST)") + pluginsCompileCmd.Flags().StringP("output", "o", "", "Set the path to save compiled plugin (matches source type, i.e. uses files or directory)") + pluginsInfoCmd.Flags().String("host", "http://localhost:5050", "Set the makeshift remote host (can be set with MAKESHIFT_HOST)") pluginsCmd.AddCommand(pluginsCompileCmd, pluginsInspectCmd, pluginsInfoCmd) rootCmd.AddCommand(pluginsCmd) diff --git a/cmd/root.go b/cmd/root.go index caa383c..fb354f0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "net/http" "os" logger "git.towk2.me/towk/makeshift/pkg/log" @@ -78,6 +79,16 @@ func setenv(cmd *cobra.Command, varname string, envvar string) { } } +func setenvp(cmd *cobra.Command, varname string, envvar string) { + if cmd.Flags().Changed(varname) { + return + } + val := os.Getenv(envvar) + if val != "" { + cmd.PersistentFlags().Set(varname, val) + } +} + func initLogger() { // initialize the logger logfile, _ := rootCmd.PersistentFlags().GetString("log-file") @@ -87,3 +98,23 @@ func initLogger() { os.Exit(1) } } + +func handleResponseError(res *http.Response, host, query string, err error) { + if err != nil { + log.Error().Err(err). + Str("host", host). + Str("query", query). + Msg("failed to make request") + os.Exit(1) + } + if res.StatusCode != http.StatusOK { + log.Error(). + Any("status", map[string]any{ + "code": res.StatusCode, + "message": res.Status, + }). + Str("host", host). + Msg("response returned bad status") + os.Exit(1) + } +} diff --git a/cmd/run.go b/cmd/run.go index 399aa83..ab698e7 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -5,14 +5,16 @@ import "github.com/spf13/cobra" var runCmd = &cobra.Command{ Use: "run", Example: ` + NOTE: This command is not implemented yet! + # set up environment export MAKESHIFT_HOST=http://localhost:5050 export MAKESHIFT_PATH=help.txt - export MAKESHIFT_ROOT=./test + export MAKESHIFT_ROOT=/opt/makeshift # run locally similar to 'download' makeshift run --plugins jinja2 --profiles default - makeshift run --root ./test -p help.txt --plugins jinja2 --profiles default + makeshift run --root $HOME/apps/makeshift -p help.txt --plugins jinja2 --profiles default `, Args: cobra.NoArgs, Short: "Run locally with plugins and profiles", diff --git a/cmd/upload.go b/cmd/upload.go index 12b58b1..ecdd638 100644 --- a/cmd/upload.go +++ b/cmd/upload.go @@ -4,72 +4,127 @@ import ( "bufio" "encoding/json" "fmt" + "net/http" "os" "strings" "git.towk2.me/towk/makeshift/internal/format" + makeshift "git.towk2.me/towk/makeshift/pkg" + "git.towk2.me/towk/makeshift/pkg/client" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) var ( inputFormat format.DataFormat = format.JSON - dataArgs []string ) var uploadCmd = &cobra.Command{ Use: "upload", Example: ` - # upload a single file - makeshift upload -d @compute-base.yaml -t file - - # upload a single file with contents without specify type - makeshift upload -d '{"name": "John Smith", "email": "john.smith@example.com"}' + # upload a single file in root directory + makeshift upload -d @compute-base.yaml # upload a directory - makeshift upload -d @setup/ -t directory + makeshift upload -d @setup/ # upload an archive (extracted and saved on server) makeshift upload -d @setup.tar.gz -t archive + + # upload a new profile with a specific path (used to set remote location) + makeshift upload profile -d @kubernetes.json -p nodes/kubernetes.json + makeshift upload profile -d @slurm.json -@compute.json -p nodes `, + Short: "Upload files and directories", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + setenv(cmd, "host", "MAKESHIFT_HOST") + setenv(cmd, "path", "MAKESHIFT_PATH") + }, Run: func(cmd *cobra.Command, args []string) { - // make one request be host positional argument (restricted to 1 for now) - var inputData []map[string]any - temp := append(handleArgs(args), processDataArgs(dataArgs)...) - for _, data := range temp { - if data != nil { - inputData = append(inputData, data) + var ( + dataArgs, _ = cmd.Flags().GetStringArray("data") + + inputData = processFiles(dataArgs) + useDirectoryPath = len(inputData) > 1 + ) + for path, contents := range inputData { + log.Info().Str("path", path).Int("size", len(contents)).Send() + if useDirectoryPath { + + } else { + } } }, } -var uploadProfileCmd = &cobra.Command{ - Use: "profile", +var uploadProfilesCmd = &cobra.Command{ + Use: "profile [profileID]", Example: ` # upload a new profile makeshift upload profile -d @compute.json - # upload a new profile with a specific name (used for lookups) + # upload a new profile with a specific path (used for lookup) makeshift upload profile -d @kubernetes.json -n k8s `, - Args: cobra.ExactArgs(1), + Args: cobra.NoArgs, Short: "Upload a new profile", Run: func(cmd *cobra.Command, args []string) { // make one request be host positional argument (restricted to 1 for now) - var inputData []map[string]any - temp := append(handleArgs(args), processDataArgs(dataArgs)...) - for _, data := range temp { - if data != nil { - inputData = append(inputData, data) + var ( + // inputData []map[string]any = append(handleArgs(args), processDataArgs(dataArgs)...) + host, _ = cmd.Flags().GetString("host") + dataArgs, _ = cmd.Flags().GetStringArray("data") + profiles = processProfiles(dataArgs) + + c = client.New(host) + res *http.Response + query string + body []byte + err error + ) + + for _, profile := range profiles { + if profile == nil { + continue + } + + body, err = json.Marshal(profile) + if err != nil { + log.Error().Err(err).Msg("failed to marshal profile") + continue + } + + // send data to server + query = fmt.Sprintf("/profiles/%s", profile.ID) + res, body, err = c.MakeRequest(client.HTTPEnvelope{ + Path: query, + Method: http.MethodPost, + Body: body, + }) + if err != nil { + log.Error().Err(err). + Str("host", host). + Str("query", query). + Msg("failed to make request") + os.Exit(1) + } + if res.StatusCode != http.StatusOK { + log.Error(). + Any("status", map[string]any{ + "code": res.StatusCode, + "message": res.Status, + }). + Str("host", host). + Msg("response returned bad status") + os.Exit(1) } } - }, } -var uploadPluginCmd = &cobra.Command{ +var uploadPluginsCmd = &cobra.Command{ Use: "plugin", Example: ` # upload a new plugin @@ -82,8 +137,12 @@ var uploadPluginCmd = &cobra.Command{ Short: "Upload a new plugin", Run: func(cmd *cobra.Command, args []string) { // make one request be host positional argument (restricted to 1 for now) - var inputData []map[string]any - temp := append(handleArgs(args), processDataArgs(dataArgs)...) + // temp := append(handleArgs(args), processDataArgs(dataArgs)...) + var ( + inputData []*makeshift.Profile + dataArgs, _ = cmd.PersistentFlags().GetStringArray("data") + ) + temp := processProfiles(dataArgs) for _, data := range temp { if data != nil { inputData = append(inputData, data) @@ -93,27 +152,62 @@ var uploadPluginCmd = &cobra.Command{ } func init() { - uploadProfileCmd.Flags().VarP(&inputFormat, "format", "F", "Set the input format for profile") + uploadCmd.PersistentFlags().String("host", "http://localhost:5050", "Set the makeshift remote host (can be set with MAKESHIFT_HOST)") + uploadCmd.PersistentFlags().StringArrayP("data", "d", []string{}, "Set the data to send to specified host (prepend @ for files)") + uploadCmd.Flags().StringP("path", "p", ".", "Set the path to list files (can be set with MAKESHIFT_PATH)") - uploadCmd.AddCommand(uploadProfileCmd, uploadPluginCmd) + uploadProfilesCmd.Flags().VarP(&inputFormat, "format", "F", "Set the input format for profile") + + uploadCmd.AddCommand(uploadProfilesCmd, uploadPluginsCmd) rootCmd.AddCommand(uploadCmd) } -// processDataArgs takes a slice of strings that check for the @ symbol and loads +func processFiles(args []string) map[string][]byte { + // load data either from file or directly from args + var collection = make(map[string][]byte, len(args)) + for _, arg := range args { + // if arg is empty string, then skip and continue + if len(arg) > 0 { + // determine if we're reading from file to load contents + if strings.HasPrefix(arg, "@") { + var ( + path string = strings.TrimLeft(arg, "@") + contents []byte + err error + ) + contents, err = os.ReadFile(path) + if err != nil { + log.Error().Err(err).Str("path", path).Msg("failed to read file") + continue + } + + // skip empty files + if len(contents) == 0 { + log.Warn().Str("path", path).Msg("file is empty") + continue + } + + // add loaded data to collection of all data + collection[path] = contents + } else { + log.Warn().Msg("only files can be uploaded") + + continue + } + } + } + return collection +} + +// processProfiles takes a slice of strings that check for the @ symbol and loads // the contents from the file specified in place (which replaces the path). // // NOTE: The purpose is to make the input arguments uniform for our request. This // function is meant to handle data passed with the `-d/--data` flag and positional // args from the CLI. -func processDataArgs(args []string) []map[string]any { - // JSON representation - type ( - JSONObject = map[string]any - JSONArray = []JSONObject - ) - +func processProfiles(args []string) []*makeshift.Profile { // load data either from file or directly from args - var collection = make(JSONArray, len(args)) + var collection = make([]*makeshift.Profile, len(args)) for i, arg := range args { // if arg is empty string, then skip and continue if len(arg) > 0 { @@ -122,7 +216,7 @@ func processDataArgs(args []string) []map[string]any { var ( path string = strings.TrimLeft(arg, "@") contents []byte - data JSONArray + data *makeshift.Profile err error ) contents, err = os.ReadFile(path) @@ -138,17 +232,17 @@ func processDataArgs(args []string) []map[string]any { } // convert/validate input data - data, err = parseInput(contents, format.DataFormatFromFileExt(path, inputFormat)) + data, err = parseProfile(contents, format.DataFormatFromFileExt(path, inputFormat)) if err != nil { log.Error().Err(err).Str("path", path).Msg("failed to validate input from file") } // add loaded data to collection of all data - collection = append(collection, data...) + collection = append(collection, data) } else { // input should be a valid JSON var ( - data JSONArray + data *makeshift.Profile input = []byte(arg) err error ) @@ -160,57 +254,23 @@ func processDataArgs(args []string) []map[string]any { if err != nil { log.Error().Err(err).Msgf("failed to unmarshal input for argument %d", i) } - return data + return []*makeshift.Profile{data} } } } return collection } -func handleArgs(args []string) []map[string]any { - // JSON representation - type ( - JSONObject = map[string]any - JSONArray = []JSONObject - ) - // no file to load, so we just use the joined args (since each one is a new line) - // and then stop +func parseProfile(contents []byte, dataFormat format.DataFormat) (*makeshift.Profile, error) { var ( - collection JSONArray - data []byte - err error - ) - - if len(dataArgs) > 0 { - return nil - } - data, err = ReadStdin() - if err != nil { - log.Error().Err(err).Msg("failed to read from standard input") - return nil - } - if len(data) == 0 { - log.Warn().Msg("no data found from standard input") - return nil - } - fmt.Println(string(data)) - collection, err = parseInput([]byte(data), inputFormat) - if err != nil { - log.Error().Err(err).Msg("failed to validate input from arg") - } - return collection -} - -func parseInput(contents []byte, dataFormat format.DataFormat) ([]map[string]any, error) { - var ( - data []map[string]any + data *makeshift.Profile err error ) // convert/validate JSON input format err = format.Unmarshal(contents, &data, dataFormat) if err != nil { - return nil, fmt.Errorf("failed to unmarshal data: %v", err) + return nil, fmt.Errorf("failed to unmarshal profile: %v", err) } return data, nil } diff --git a/pkg/client/client.go b/pkg/client/client.go index f9cf5d9..028fced 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -1,12 +1,16 @@ package client import ( + "crypto/tls" + "crypto/x509" "fmt" "io" "mime/multipart" + "net" "net/http" "os" "strings" + "time" "git.towk2.me/towk/makeshift/pkg/util" "github.com/cavaliergopher/grab/v3" @@ -107,6 +111,43 @@ func (c *Client) UploadMultipartFile(uri, key, path string) (*http.Response, err return resp, nil } +func (c *Client) LoadCertificateFromPath(path string) error { + cacert, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read certificate at path: %s", path) + } + certPool := x509.NewCertPool() + certPool.AppendCertsFromPEM(cacert) + err = c.LoadCertificateFromPool(certPool) + if err != nil { + return fmt.Errorf("could not initialize certificate from pool: %v", err) + } + return nil +} + +func (c *Client) LoadCertificateFromPool(certPool *x509.CertPool) error { + // make sure we have a valid cert pool + if certPool == nil { + return fmt.Errorf("invalid cert pool") + } + + // make sure that we can access the internal client + c.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: certPool, + InsecureSkipVerify: false, + }, + DisableKeepAlives: true, + Dial: (&net.Dialer{ + Timeout: 120 * time.Second, + KeepAlive: 120 * time.Second, + }).Dial, + TLSHandshakeTimeout: 120 * time.Second, + ResponseHeaderTimeout: 120 * time.Second, + } + return nil +} + func mustOpen(f string) *os.File { r, err := os.Open(f) if err != nil { diff --git a/pkg/service/plugins.go b/pkg/service/plugins.go index 0dd516e..0ab12eb 100644 --- a/pkg/service/plugins.go +++ b/pkg/service/plugins.go @@ -8,6 +8,7 @@ import ( makeshift "git.towk2.me/towk/makeshift/pkg" "github.com/go-chi/chi/v5" + "github.com/rs/zerolog/log" ) func (s *Service) ListPlugins() http.HandlerFunc { @@ -124,24 +125,17 @@ func (s *Service) CreatePlugin() http.HandlerFunc { func (s *Service) DeletePlugin() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var ( - path string - plugin makeshift.Plugin - err error + pluginName = chi.URLParam(r, "name") + path = s.PathForPluginWithName(pluginName) + err error ) - plugin, err = getPluginFromRequestBody(r) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - path = s.PathForPluginWithName(plugin.Name()) + log.Debug().Str("path", path).Send() err = os.Remove(path) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - w.WriteHeader(http.StatusOK) } } diff --git a/pkg/service/profiles.go b/pkg/service/profiles.go index edef023..e951e26 100644 --- a/pkg/service/profiles.go +++ b/pkg/service/profiles.go @@ -88,13 +88,10 @@ func (s *Service) GetProfile() http.HandlerFunc { func (s *Service) CreateProfile() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - type input struct { - Path string `json:"path"` - Profile *makeshift.Profile `json:"profile"` - } var ( body, contents []byte - in input + path string + profile *makeshift.Profile err error ) @@ -105,23 +102,24 @@ func (s *Service) CreateProfile() http.HandlerFunc { } // use the request info to build profile - err = json.Unmarshal(body, &in) + err = json.Unmarshal(body, &profile) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, fmt.Sprintf("failed to unmarshal profile: %v", err.Error()), http.StatusBadRequest) return } // serialize just the profile part - contents, err = json.Marshal(in.Profile) + contents, err = json.Marshal(profile) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, fmt.Sprintf("failed to marshal profile: %v", err.Error()), http.StatusBadRequest) return } // create a new profile on disk - err = os.WriteFile(in.Path, contents, os.ModePerm) + path = s.PathForProfileWithID(profile.ID) + err = os.WriteFile(path, contents, os.ModePerm) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -129,6 +127,29 @@ func (s *Service) CreateProfile() http.HandlerFunc { } } +func (s *Service) DeleteProfile() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var ( + profileID = chi.URLParam(r, "id") + path string + err error + ) + + if profileID == "default" { + http.Error(w, "cannot delete the default profile", http.StatusBadRequest) + return + } + + path = s.PathForProfileWithID(profileID) + err = os.Remove(path) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + } +} + func (s *Service) SetProfileData() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var ( diff --git a/pkg/service/routes.go b/pkg/service/routes.go index 42e7338..23d2d05 100644 --- a/pkg/service/routes.go +++ b/pkg/service/routes.go @@ -181,18 +181,9 @@ func (s *Service) Download() http.HandlerFunc { func (s *Service) Upload() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - - } -} - -func (s *Service) UploadPlugin() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - - } -} - -func (s *Service) UploadProfile() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { + var ( + _ = s.PathForData() + strings.TrimPrefix(r.URL.Path, "/upload") + ) } } @@ -239,6 +230,23 @@ func (s *Service) List() http.HandlerFunc { } } +func (s *Service) Delete() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var ( + path = s.PathForData() + strings.TrimPrefix(r.URL.Path, "/delete") + err error + ) + + err = os.RemoveAll(path) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + } +} + func (s *Service) GetStatus(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(map[string]any{ diff --git a/pkg/service/service.go b/pkg/service/service.go index b75bd57..362a08c 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -98,15 +98,15 @@ func (s *Service) Serve() error { } else { // general router.Get("/download/*", s.Download()) - router.Post("/upload/", s.Upload()) - router.Post("/upload/plugin", s.UploadPlugin()) - router.Post("/upload/profile", s.UploadProfile()) + router.Post("/upload/*", s.Upload()) router.Get("/list/*", s.List()) + router.Delete("/delete/*", s.Delete()) // profiles router.Get("/profiles", s.ListProfiles()) router.Get("/profiles/{id}", s.GetProfile()) router.Post("/profiles/{id}", s.CreateProfile()) + router.Delete("/profiles/{id}", s.DeleteProfile()) router.Get("/profiles/{id}/data", s.GetProfileData()) router.Post("/profiles/{id}/data", s.SetProfileData()) router.Delete("/profiles/{id}/data", s.DeleteProfileData())