github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/charmhub/refreshconfig.go (about) 1 // Copyright 2021 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package charmhub 5 6 import ( 7 "fmt" 8 "strings" 9 10 "github.com/juju/collections/set" 11 "github.com/juju/errors" 12 13 "github.com/juju/juju/charmhub/transport" 14 ) 15 16 // RefreshConfig defines a type for building refresh requests. 17 type RefreshConfig interface { 18 // Build a refresh request for sending to the API. 19 Build() (transport.RefreshRequest, error) 20 21 // Ensure that the request back contains the information we requested. 22 Ensure([]transport.RefreshResponse) error 23 24 // String describes the underlying refresh config. 25 String() string 26 } 27 28 // refreshOne holds the config for making refresh calls to the CharmHub API. 29 type refreshOne struct { 30 ID string 31 Revision int 32 Channel string 33 Base RefreshBase 34 // instanceKey is a private unique key that we construct for CharmHub API 35 // asynchronous calls. 36 instanceKey string 37 metrics transport.ContextMetrics 38 fields []string 39 } 40 41 // InstanceKey returns the underlying instance key. 42 func (c refreshOne) InstanceKey() string { 43 return c.instanceKey 44 } 45 46 func (c refreshOne) String() string { 47 return fmt.Sprintf("Refresh one (instanceKey: %s): using ID %s revision %+v, with channel %s and base %v", 48 c.instanceKey, c.ID, c.Revision, c.Channel, c.Base.String()) 49 } 50 51 // Build a refresh request that can be past to the API. 52 func (c refreshOne) Build() (transport.RefreshRequest, error) { 53 base, err := constructRefreshBase(c.Base) 54 if err != nil { 55 return transport.RefreshRequest{}, errors.Trace(err) 56 } 57 58 return transport.RefreshRequest{ 59 Context: []transport.RefreshRequestContext{{ 60 InstanceKey: c.instanceKey, 61 ID: c.ID, 62 Revision: c.Revision, 63 Base: base, 64 TrackingChannel: c.Channel, 65 Metrics: c.metrics, 66 // TODO (stickupkid): We need to model the refreshed date. It's 67 // currently optional, but will be required at some point. This 68 // is the installed date of the charm on the system. 69 }}, 70 Actions: []transport.RefreshRequestAction{{ 71 Action: string(refreshAction), 72 InstanceKey: c.instanceKey, 73 ID: &c.ID, 74 }}, 75 Fields: c.fields, 76 }, nil 77 } 78 79 // Ensure that the request back contains the information we requested. 80 func (c refreshOne) Ensure(responses []transport.RefreshResponse) error { 81 for _, resp := range responses { 82 if resp.InstanceKey == c.instanceKey { 83 return nil 84 } 85 } 86 return errors.NotValidf("refresh action key") 87 } 88 89 type executeOne struct { 90 ID string 91 Name string 92 Revision *int 93 Channel *string 94 Base RefreshBase 95 // instanceKey is a private unique key that we construct for CharmHub API 96 // asynchronous calls. 97 action action 98 instanceKey string 99 fields []string 100 } 101 102 // InstanceKey returns the underlying instance key. 103 func (c executeOne) InstanceKey() string { 104 return c.instanceKey 105 } 106 107 // Build a refresh request that can be past to the API. 108 func (c executeOne) Build() (transport.RefreshRequest, error) { 109 base, err := constructRefreshBase(c.Base) 110 if err != nil { 111 return transport.RefreshRequest{}, errors.Trace(err) 112 } 113 114 var id *string 115 if c.ID != "" { 116 id = &c.ID 117 } 118 var name *string 119 if c.Name != "" { 120 name = &c.Name 121 } 122 123 req := transport.RefreshRequest{ 124 // Context is required here, even if it looks optional. 125 Context: []transport.RefreshRequestContext{}, 126 Actions: []transport.RefreshRequestAction{{ 127 Action: string(c.action), 128 InstanceKey: c.instanceKey, 129 ID: id, 130 Name: name, 131 Revision: c.Revision, 132 Channel: c.Channel, 133 Base: &base, 134 }}, 135 Fields: c.fields, 136 } 137 return req, nil 138 } 139 140 // Ensure that the request back contains the information we requested. 141 func (c executeOne) Ensure(responses []transport.RefreshResponse) error { 142 for _, resp := range responses { 143 if resp.InstanceKey == c.instanceKey { 144 return nil 145 } 146 } 147 return errors.NotValidf("%v action key", string(c.action)) 148 } 149 150 func (c executeOne) String() string { 151 var channel string 152 if c.Channel != nil { 153 channel = *c.Channel 154 } 155 var using string 156 if c.ID != "" { 157 using = fmt.Sprintf("ID %s", c.ID) 158 } else { 159 using = fmt.Sprintf("Name %s", c.Name) 160 } 161 var revision string 162 if c.Revision != nil { 163 revision = fmt.Sprintf(" with revision: %+v", c.Revision) 164 } 165 return fmt.Sprintf("Execute One (action: %s, instanceKey: %s): using %s%s channel %v and base %s", 166 c.action, c.instanceKey, using, revision, channel, c.Base) 167 } 168 169 type executeOneByRevision struct { 170 Name string 171 Revision *int 172 // ID is only used for download by revision 173 ID string 174 resourceRevisions []transport.RefreshResourceRevision 175 // instanceKey is a private unique key that we construct for CharmHub API 176 // asynchronous calls. 177 instanceKey string 178 action action 179 fields []string 180 } 181 182 // InstanceKey returns the underlying instance key. 183 func (c executeOneByRevision) InstanceKey() string { 184 return c.instanceKey 185 } 186 187 // Build a refresh request for sending to the API. 188 func (c executeOneByRevision) Build() (transport.RefreshRequest, error) { 189 var name, id *string 190 if c.Name != "" { 191 name = &c.Name 192 } 193 if c.ID != "" { 194 id = &c.ID 195 } 196 197 req := transport.RefreshRequest{ 198 // Context is required here, even if it looks optional. 199 Context: []transport.RefreshRequestContext{}, 200 Actions: []transport.RefreshRequestAction{{ 201 Action: string(c.action), 202 InstanceKey: c.instanceKey, 203 Name: name, 204 ID: id, 205 Revision: c.Revision, 206 ResourceRevisions: c.resourceRevisions, 207 }}, 208 Fields: []string{"bases", "download", "id", "revision", "version", "resources", "type"}, 209 } 210 211 if len(c.fields) != 0 { 212 fieldSet := set.NewStrings(req.Fields...) 213 for _, field := range c.fields { 214 fieldSet.Add(field) 215 } 216 req.Fields = fieldSet.SortedValues() 217 } 218 219 return req, nil 220 } 221 222 // Ensure that the request back contains the information we requested. 223 func (c executeOneByRevision) Ensure(responses []transport.RefreshResponse) error { 224 for _, resp := range responses { 225 if resp.InstanceKey == c.instanceKey { 226 return nil 227 } 228 } 229 return errors.NotValidf("%v action key", string(c.action)) 230 } 231 232 // String describes the underlying refresh config. 233 func (c executeOneByRevision) String() string { 234 var revision string 235 if c.Revision != nil { 236 revision = fmt.Sprintf(" with revision: %+v", c.Revision) 237 } 238 return fmt.Sprintf("Install One (action: %s, instanceKey: %s): using Name %s %s", 239 c.action, c.instanceKey, c.Name, revision) 240 } 241 242 type refreshMany struct { 243 Configs []RefreshConfig 244 } 245 246 // RefreshMany will compose many refresh configs. 247 func RefreshMany(configs ...RefreshConfig) RefreshConfig { 248 return refreshMany{ 249 Configs: configs, 250 } 251 } 252 253 // Build a refresh request that can be past to the API. 254 func (c refreshMany) Build() (transport.RefreshRequest, error) { 255 if len(c.Configs) == 0 { 256 return transport.RefreshRequest{}, errors.NotFoundf("configs") 257 } 258 // Not all configs built here have a context, start out with an empty 259 // slice, so we do not call Refresh with a nil context. 260 // See executeOne.Build(). 261 result := transport.RefreshRequest{ 262 Context: []transport.RefreshRequestContext{}, 263 } 264 for _, config := range c.Configs { 265 req, err := config.Build() 266 if err != nil { 267 return transport.RefreshRequest{}, errors.Trace(err) 268 } 269 result.Context = append(result.Context, req.Context...) 270 result.Actions = append(result.Actions, req.Actions...) 271 result.Fields = append(result.Fields, req.Fields...) 272 } 273 274 // Ensure that the required field list contains no duplicates 275 if len(result.Fields) != 0 { 276 result.Fields = set.NewStrings(result.Fields...).SortedValues() 277 } 278 279 return result, nil 280 } 281 282 // Ensure that the request back contains the information we requested. 283 func (c refreshMany) Ensure(responses []transport.RefreshResponse) error { 284 for _, config := range c.Configs { 285 if err := config.Ensure(responses); err != nil { 286 return errors.Annotatef(err, "missing response") 287 } 288 } 289 return nil 290 } 291 292 func (c refreshMany) String() string { 293 plans := make([]string, len(c.Configs)) 294 for i, config := range c.Configs { 295 plans[i] = config.String() 296 } 297 return strings.Join(plans, "\n") 298 }