github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/retention-common.go (about) 1 // Copyright (c) 2015-2022 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "context" 22 "fmt" 23 "strconv" 24 "time" 25 26 json "github.com/minio/colorjson" 27 "github.com/minio/mc/pkg/probe" 28 "github.com/minio/minio-go/v7" 29 "github.com/minio/pkg/v2/console" 30 ) 31 32 // Structured message depending on the type of console. 33 type retentionCmdMessage struct { 34 Op lockOpType `json:"op"` 35 Mode minio.RetentionMode `json:"mode"` 36 Validity string `json:"validity"` 37 URLPath string `json:"urlpath"` 38 VersionID string `json:"versionID"` 39 Status string `json:"status"` 40 Err error `json:"error"` 41 } 42 43 // Colorized message for console printing. 44 func (m retentionCmdMessage) String() string { 45 var color, msg string 46 ed := "" 47 if m.Op == lockOpClear { 48 ed = "ed" 49 } 50 51 if m.Err != nil { 52 color = "RetentionFailure" 53 msg = fmt.Sprintf("Unable to %s object retention on `%s`: %s", m.Op, m.URLPath, m.Err) 54 } else { 55 color = "RetentionSuccess" 56 msg = fmt.Sprintf("Object retention successfully %s%s for `%s`", m.Op, ed, m.URLPath) 57 } 58 if m.VersionID != "" { 59 msg += fmt.Sprintf(" (version-id=%s)", m.VersionID) 60 } 61 msg += "." 62 return console.Colorize(color, msg) 63 } 64 65 // JSON'ified message for scripting. 66 func (m retentionCmdMessage) JSON() string { 67 if m.Err != nil { 68 m.Status = "failure" 69 } 70 msgBytes, e := json.MarshalIndent(m, "", " ") 71 fatalIf(probe.NewError(e), "Unable to marshal into JSON.") 72 return string(msgBytes) 73 } 74 75 type lockOpType string 76 77 const ( 78 lockOpInfo = "info" 79 lockOpClear = "clear" 80 lockOpSet = "set" 81 ) 82 83 // Structured message depending on the type of console. 84 type retentionBucketMessage struct { 85 Op lockOpType `json:"op"` 86 Enabled string `json:"enabled"` 87 Mode minio.RetentionMode `json:"mode"` 88 Validity string `json:"validity"` 89 Status string `json:"status"` 90 } 91 92 // Colorized message for console printing. 93 func (m retentionBucketMessage) String() string { 94 if m.Op == lockOpClear { 95 return console.Colorize("RetentionSuccess", "Object lock configuration cleared successfully.") 96 } 97 // info/set command 98 if !m.Mode.IsValid() { 99 return console.Colorize("RetentionNotFound", "Object locking is not enabled.") 100 } 101 return console.Colorize("RetentionSuccess", fmt.Sprintf("Object locking '%s' is configured for %s.", 102 console.Colorize("Mode", m.Mode), console.Colorize("Validity", m.Validity))) 103 } 104 105 // JSON'ified message for scripting. 106 func (m retentionBucketMessage) JSON() string { 107 msgBytes, e := json.MarshalIndent(m, "", " ") 108 fatalIf(probe.NewError(e), "Unable to marshal into JSON.") 109 return string(msgBytes) 110 } 111 112 func getRetainUntilDate(validity uint64, unit minio.ValidityUnit) (string, *probe.Error) { 113 if validity == 0 { 114 return "", probe.NewError(fmt.Errorf("invalid validity '%v'", validity)) 115 } 116 t := UTCNow() 117 if unit == minio.Years { 118 t = t.AddDate(int(validity), 0, 0) 119 } else { 120 t = t.AddDate(0, 0, int(validity)) 121 } 122 timeStr := t.Format(time.RFC3339) 123 124 return timeStr, nil 125 } 126 127 func setRetentionSingle(ctx context.Context, op lockOpType, alias, url, versionID string, mode minio.RetentionMode, retainUntil time.Time, bypassGovernance bool) *probe.Error { 128 newClnt, err := newClientFromAlias(alias, url) 129 if err != nil { 130 return err 131 } 132 133 msg := retentionCmdMessage{ 134 Op: op, 135 Mode: mode, 136 URLPath: urlJoinPath(alias, url), 137 VersionID: versionID, 138 } 139 140 err = newClnt.PutObjectRetention(ctx, versionID, mode, retainUntil, bypassGovernance) 141 if err != nil { 142 msg.Err = err.ToGoError() 143 msg.Status = "failure" 144 } else { 145 msg.Status = "success" 146 } 147 148 printMsg(msg) 149 return err 150 } 151 152 func parseRetentionValidity(validityStr string) (uint64, minio.ValidityUnit, *probe.Error) { 153 unitStr := string(validityStr[len(validityStr)-1]) 154 validityStr = validityStr[:len(validityStr)-1] 155 validity, e := strconv.ParseUint(validityStr, 10, 64) 156 if e != nil { 157 return 0, "", probe.NewError(e).Trace(validityStr) 158 } 159 160 var unit minio.ValidityUnit 161 switch unitStr { 162 case "d", "D": 163 unit = minio.Days 164 case "y", "Y": 165 unit = minio.Years 166 default: 167 return 0, "", errInvalidArgument().Trace(unitStr) 168 } 169 170 return validity, unit, nil 171 } 172 173 func fatalIfBucketLockNotSupported(ctx context.Context, aliasedURL string) { 174 _, err := getBucketLockStatus(ctx, aliasedURL) 175 if err != nil { 176 fatalIf(errDummy().Trace(), "Remote bucket `%s` does not support locking", aliasedURL) 177 } 178 } 179 180 // Apply Retention for one object/version or many objects within a given prefix. 181 func applyRetention(ctx context.Context, op lockOpType, target, versionID string, timeRef time.Time, withOlderVersions, isRecursive bool, 182 mode minio.RetentionMode, validity uint64, unit minio.ValidityUnit, bypassGovernance bool, 183 ) error { 184 clnt, err := newClient(target) 185 if err != nil { 186 fatalIf(err.Trace(), "Unable to parse the provided url.") 187 } 188 189 // Quit early if urlStr does not point to an S3 server 190 switch clnt.(type) { 191 case *S3Client: 192 default: 193 fatal(errDummy().Trace(), "Retention is supported only for S3 servers.") 194 } 195 196 var until time.Time 197 if mode != "" { 198 timeStr, err := getRetainUntilDate(validity, unit) 199 if err != nil { 200 return err.ToGoError() 201 } 202 var e error 203 until, e = time.Parse(time.RFC3339, timeStr) 204 if e != nil { 205 return e 206 } 207 } 208 209 alias, urlStr, _ := mustExpandAlias(target) 210 if versionID != "" || !isRecursive && !withOlderVersions { 211 err := setRetentionSingle(ctx, op, alias, urlStr, versionID, mode, until, bypassGovernance) 212 fatalIf(err.Trace(), "Unable to set retention on `%s`", target) 213 return nil 214 } 215 216 lstOptions := ListOptions{Recursive: isRecursive, ShowDir: DirNone} 217 if !timeRef.IsZero() { 218 lstOptions.WithOlderVersions = withOlderVersions 219 lstOptions.WithDeleteMarkers = true 220 lstOptions.TimeRef = timeRef 221 } 222 223 var cErr error 224 var atLeastOneRetentionApplied bool 225 226 for content := range clnt.List(ctx, lstOptions) { 227 if content.Err != nil { 228 errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.") 229 cErr = exitStatus(globalErrorExitStatus) // Set the exit status. 230 continue 231 } 232 233 // The spec does not allow setting retention on delete marker 234 if content.IsDeleteMarker { 235 continue 236 } 237 238 if !isRecursive && alias+getKey(content) != getStandardizedURL(target) { 239 break 240 } 241 242 err := setRetentionSingle(ctx, op, alias, content.URL.String(), content.VersionID, mode, until, bypassGovernance) 243 if err != nil { 244 errorIf(err.Trace(clnt.GetURL().String()), "Invalid URL") 245 continue 246 } 247 248 atLeastOneRetentionApplied = true 249 } 250 251 if !atLeastOneRetentionApplied { 252 errorIf(errDummy().Trace(clnt.GetURL().String()), "Unable to find any object/version to "+string(op)+" its retention.") 253 cErr = exitStatus(globalErrorExitStatus) // Set the exit status. 254 } 255 256 return cErr 257 } 258 259 // applyBucketLock - set object lock configuration. 260 func applyBucketLock(op lockOpType, urlStr string, mode minio.RetentionMode, validity uint64, unit minio.ValidityUnit) error { 261 client, err := newClient(urlStr) 262 if err != nil { 263 fatalIf(err.Trace(), "Unable to parse the provided url.") 264 } 265 266 ctx, cancelLock := context.WithCancel(globalContext) 267 defer cancelLock() 268 if op == lockOpClear || mode != "" { 269 err = client.SetObjectLockConfig(ctx, mode, validity, unit) 270 fatalIf(err, "Unable to apply bucket lock configuration.") 271 } else { 272 _, mode, validity, unit, err = client.GetObjectLockConfig(ctx) 273 fatalIf(err, "Unable to apply bucket lock configuration.") 274 } 275 276 printMsg(retentionBucketMessage{ 277 Op: op, 278 Enabled: "Enabled", 279 Mode: mode, 280 Validity: fmt.Sprintf("%d%s", validity, unit), 281 Status: "success", 282 }) 283 284 return nil 285 } 286 287 // showBucketLock - show object lock configuration. 288 func showBucketLock(urlStr string) error { 289 client, err := newClient(urlStr) 290 if err != nil { 291 fatalIf(err.Trace(), "Unable to parse the provided url.") 292 } 293 294 ctx, cancelLock := context.WithCancel(globalContext) 295 defer cancelLock() 296 297 status, mode, validity, unit, err := client.GetObjectLockConfig(ctx) 298 fatalIf(err, "Unable to get bucket lock configuration.") 299 300 printMsg(retentionBucketMessage{ 301 Op: lockOpInfo, 302 Enabled: status, 303 Mode: mode, 304 Validity: fmt.Sprintf("%d%s", validity, unit), 305 Status: "success", 306 }) 307 308 return nil 309 }