1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
|
package controlapi
import (
"bytes"
"context"
"strings"
"github.com/moby/swarmkit/v2/api"
"github.com/moby/swarmkit/v2/identity"
"github.com/moby/swarmkit/v2/log"
"github.com/moby/swarmkit/v2/manager/state/store"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// MaxConfigSize is the maximum byte length of the `Config.Spec.Data` field.
const MaxConfigSize = 1000 * 1024 // 1000KB
// assumes spec is not nil
func configFromConfigSpec(spec *api.ConfigSpec) *api.Config {
return &api.Config{
ID: identity.NewID(),
Spec: *spec,
}
}
// GetConfig returns a `GetConfigResponse` with a `Config` with the same
// id as `GetConfigRequest.ConfigID`
// - Returns `NotFound` if the Config with the given id is not found.
// - Returns `InvalidArgument` if the `GetConfigRequest.ConfigID` is empty.
// - Returns an error if getting fails.
func (s *Server) GetConfig(ctx context.Context, request *api.GetConfigRequest) (*api.GetConfigResponse, error) {
if request.ConfigID == "" {
return nil, status.Errorf(codes.InvalidArgument, "config ID must be provided")
}
var config *api.Config
s.store.View(func(tx store.ReadTx) {
config = store.GetConfig(tx, request.ConfigID)
})
if config == nil {
return nil, status.Errorf(codes.NotFound, "config %s not found", request.ConfigID)
}
return &api.GetConfigResponse{Config: config}, nil
}
// UpdateConfig updates a Config referenced by ConfigID with the given ConfigSpec.
// - Returns `NotFound` if the Config is not found.
// - Returns `InvalidArgument` if the ConfigSpec is malformed or anything other than Labels is changed
// - Returns an error if the update fails.
func (s *Server) UpdateConfig(ctx context.Context, request *api.UpdateConfigRequest) (*api.UpdateConfigResponse, error) {
if request.ConfigID == "" || request.ConfigVersion == nil {
return nil, status.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
}
var config *api.Config
err := s.store.Update(func(tx store.Tx) error {
config = store.GetConfig(tx, request.ConfigID)
if config == nil {
return status.Errorf(codes.NotFound, "config %s not found", request.ConfigID)
}
// Check if the Name is different than the current name, or the config is non-nil and different
// than the current config
if config.Spec.Annotations.Name != request.Spec.Annotations.Name ||
(request.Spec.Data != nil && !bytes.Equal(request.Spec.Data, config.Spec.Data)) {
return status.Errorf(codes.InvalidArgument, "only updates to Labels are allowed")
}
// We only allow updating Labels
config.Meta.Version = *request.ConfigVersion
config.Spec.Annotations.Labels = request.Spec.Annotations.Labels
return store.UpdateConfig(tx, config)
})
if err != nil {
return nil, err
}
log.G(ctx).WithFields(log.Fields{
"config.ID": request.ConfigID,
"config.Name": request.Spec.Annotations.Name,
"method": "UpdateConfig",
}).Debugf("config updated")
return &api.UpdateConfigResponse{
Config: config,
}, nil
}
// ListConfigs returns a `ListConfigResponse` with a list all non-internal `Config`s being
// managed, or all configs matching any name in `ListConfigsRequest.Names`, any
// name prefix in `ListConfigsRequest.NamePrefixes`, any id in
// `ListConfigsRequest.ConfigIDs`, or any id prefix in `ListConfigsRequest.IDPrefixes`.
// - Returns an error if listing fails.
func (s *Server) ListConfigs(ctx context.Context, request *api.ListConfigsRequest) (*api.ListConfigsResponse, error) {
var (
configs []*api.Config
respConfigs []*api.Config
err error
byFilters []store.By
by store.By
labels map[string]string
)
// return all configs that match either any of the names or any of the name prefixes (why would you give both?)
if request.Filters != nil {
for _, name := range request.Filters.Names {
byFilters = append(byFilters, store.ByName(name))
}
for _, prefix := range request.Filters.NamePrefixes {
byFilters = append(byFilters, store.ByNamePrefix(prefix))
}
for _, prefix := range request.Filters.IDPrefixes {
byFilters = append(byFilters, store.ByIDPrefix(prefix))
}
labels = request.Filters.Labels
}
switch len(byFilters) {
case 0:
by = store.All
case 1:
by = byFilters[0]
default:
by = store.Or(byFilters...)
}
s.store.View(func(tx store.ReadTx) {
configs, err = store.FindConfigs(tx, by)
})
if err != nil {
return nil, err
}
// filter by label
for _, config := range configs {
if !filterMatchLabels(config.Spec.Annotations.Labels, labels) {
continue
}
respConfigs = append(respConfigs, config)
}
return &api.ListConfigsResponse{Configs: respConfigs}, nil
}
// CreateConfig creates and returns a `CreateConfigResponse` with a `Config` based
// on the provided `CreateConfigRequest.ConfigSpec`.
// - Returns `InvalidArgument` if the `CreateConfigRequest.ConfigSpec` is malformed,
// or if the config data is too long or contains invalid characters.
// - Returns an error if the creation fails.
func (s *Server) CreateConfig(ctx context.Context, request *api.CreateConfigRequest) (*api.CreateConfigResponse, error) {
if err := validateConfigSpec(request.Spec); err != nil {
return nil, err
}
config := configFromConfigSpec(request.Spec) // the store will handle name conflicts
err := s.store.Update(func(tx store.Tx) error {
return store.CreateConfig(tx, config)
})
switch err {
case store.ErrNameConflict:
return nil, status.Errorf(codes.AlreadyExists, "config %s already exists", request.Spec.Annotations.Name)
case nil:
log.G(ctx).WithFields(log.Fields{
"config.Name": request.Spec.Annotations.Name,
"method": "CreateConfig",
}).Debugf("config created")
return &api.CreateConfigResponse{Config: config}, nil
default:
return nil, err
}
}
// RemoveConfig removes the config referenced by `RemoveConfigRequest.ID`.
// - Returns `InvalidArgument` if `RemoveConfigRequest.ID` is empty.
// - Returns `NotFound` if the a config named `RemoveConfigRequest.ID` is not found.
// - Returns `ConfigInUse` if the config is currently in use
// - Returns an error if the deletion fails.
func (s *Server) RemoveConfig(ctx context.Context, request *api.RemoveConfigRequest) (*api.RemoveConfigResponse, error) {
if request.ConfigID == "" {
return nil, status.Errorf(codes.InvalidArgument, "config ID must be provided")
}
err := s.store.Update(func(tx store.Tx) error {
// Check if the config exists
config := store.GetConfig(tx, request.ConfigID)
if config == nil {
return status.Errorf(codes.NotFound, "could not find config %s", request.ConfigID)
}
// Check if any services currently reference this config, return error if so
services, err := store.FindServices(tx, store.ByReferencedConfigID(request.ConfigID))
if err != nil {
return status.Errorf(codes.Internal, "could not find services using config %s: %v", request.ConfigID, err)
}
if len(services) != 0 {
serviceNames := make([]string, 0, len(services))
for _, service := range services {
serviceNames = append(serviceNames, service.Spec.Annotations.Name)
}
configName := config.Spec.Annotations.Name
serviceNameStr := strings.Join(serviceNames, ", ")
serviceStr := "services"
if len(serviceNames) == 1 {
serviceStr = "service"
}
return status.Errorf(codes.InvalidArgument, "config '%s' is in use by the following %s: %v", configName, serviceStr, serviceNameStr)
}
return store.DeleteConfig(tx, request.ConfigID)
})
switch err {
case store.ErrNotExist:
return nil, status.Errorf(codes.NotFound, "config %s not found", request.ConfigID)
case nil:
log.G(ctx).WithFields(log.Fields{
"config.ID": request.ConfigID,
"method": "RemoveConfig",
}).Debugf("config removed")
return &api.RemoveConfigResponse{}, nil
default:
return nil, err
}
}
func validateConfigSpec(spec *api.ConfigSpec) error {
if spec == nil {
return status.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
}
if err := validateConfigOrSecretAnnotations(spec.Annotations); err != nil {
return err
}
if len(spec.Data) >= MaxConfigSize || len(spec.Data) < 1 {
return status.Errorf(codes.InvalidArgument, "config data must be larger than 0 and less than %d bytes", MaxConfigSize)
}
return nil
}
|