github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/retention-info.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 "strings" 24 "time" 25 26 "github.com/fatih/color" 27 "github.com/minio/cli" 28 json "github.com/minio/colorjson" 29 "github.com/minio/mc/pkg/probe" 30 "github.com/minio/minio-go/v7" 31 "github.com/minio/pkg/v2/console" 32 ) 33 34 var retentionInfoFlags = []cli.Flag{ 35 cli.BoolFlag{ 36 Name: "recursive, r", 37 Usage: "show retention info recursively", 38 }, 39 cli.StringFlag{ 40 Name: "version-id, vid", 41 Usage: "show retention info of specific object version", 42 }, 43 cli.StringFlag{ 44 Name: "rewind", 45 Usage: "roll back object(s) to current version at specified time", 46 }, 47 cli.BoolFlag{ 48 Name: "versions", 49 Usage: "show retention info on object(s) and all its versions", 50 }, 51 cli.BoolFlag{ 52 Name: "default", 53 Usage: "show bucket default retention mode", 54 }, 55 } 56 57 var retentionInfoCmd = cli.Command{ 58 Name: "info", 59 Usage: "show retention settings on object(s)", 60 Action: mainRetentionInfo, 61 OnUsageError: onUsageError, 62 Before: setGlobalsFromContext, 63 Flags: append(retentionInfoFlags, globalFlags...), 64 CustomHelpTemplate: `NAME: 65 {{.HelpName}} - {{.Usage}} 66 67 USAGE: 68 {{.HelpName}} [FLAGS] [governance | compliance] VALIDITY TARGET 69 70 FLAGS: 71 {{range .VisibleFlags}}{{.}} 72 {{end}} 73 74 EXAMPLES: 75 1. Show object retention for a specific object 76 $ {{.HelpName}} myminio/mybucket/prefix/obj.csv 77 78 2. Show object retention for recursively for all objects at a given prefix 79 $ {{.HelpName}} myminio/mybucket/prefix --recursive 80 81 3. Show object retention to a specific version of a specific object 82 $ {{.HelpName}} myminio/mybucket/prefix/obj.csv --version-id "3Jr2x6fqlBUsVzbvPihBO3HgNpgZgAnp" 83 84 4. Show object retention for recursively for all versions of all objects under prefix 85 $ {{.HelpName}} myminio/mybucket/prefix --recursive --versions 86 87 5. Show default lock retention configuration for a bucket 88 $ {{.HelpName}} myminio/mybucket/ --default 89 `, 90 } 91 92 func parseInfoRetentionArgs(cliCtx *cli.Context) (target, versionID string, recursive bool, timeRef time.Time, withVersions, defaultMode bool) { 93 args := cliCtx.Args() 94 95 if len(args) != 1 { 96 showCommandHelpAndExit(cliCtx, 1) 97 } 98 99 target = args[0] 100 if target == "" { 101 fatalIf(errInvalidArgument().Trace(), "invalid target url '%v'", target) 102 } 103 104 versionID = cliCtx.String("version-id") 105 timeRef = parseRewindFlag(cliCtx.String("rewind")) 106 withVersions = cliCtx.Bool("versions") 107 recursive = cliCtx.Bool("recursive") 108 defaultMode = cliCtx.Bool("default") 109 110 if defaultMode && (versionID != "" || !timeRef.IsZero() || withVersions || recursive) { 111 fatalIf(errDummy(), "--default flag cannot be specified with any of --version-id, --rewind, --versions, --recursive.") 112 } 113 114 return 115 } 116 117 // Structured message depending on the type of console. 118 type retentionInfoMessage struct { 119 Mode minio.RetentionMode `json:"mode"` 120 Until time.Time `json:"until"` 121 URLPath string `json:"urlpath"` 122 VersionID string `json:"versionID"` 123 Status string `json:"status"` 124 Err error `json:"error"` 125 } 126 127 type retentionInfoMessageList retentionInfoMessage 128 129 func (m *retentionInfoMessageList) SetErr(e error) { 130 m.Err = e 131 } 132 133 func (m *retentionInfoMessageList) SetStatus(status string) { 134 m.Status = status 135 } 136 137 func (m *retentionInfoMessageList) SetMode(mode minio.RetentionMode) { 138 m.Mode = mode 139 } 140 141 func (m *retentionInfoMessageList) SetUntil(until time.Time) { 142 m.Until = until 143 } 144 145 // Colorized message for console printing. 146 func (m retentionInfoMessageList) String() string { 147 if m.Err != nil { 148 return console.Colorize("RetentionFailure", fmt.Sprintf("Unable to get get object retention on `%s`: %s", m.URLPath, m.Err)) 149 } 150 151 var msg string 152 var retentionField string 153 154 if m.Mode == "" { 155 retentionField += console.Colorize("RetentionNotFound", "NO RETENTION") 156 } else { 157 exp := "" 158 if m.Mode == minio.Governance { 159 now := time.Now() 160 if now.After(m.Until) { 161 exp = "EXPIRED" 162 } 163 } 164 retentionField += console.Colorize("RetentionSuccess", m.Mode.String()) + " " + console.Colorize("RetentionExpired", exp) 165 } 166 167 msg += "[ " + centerText(retentionField, 18) + " ] " 168 169 if m.VersionID != "" { 170 msg += console.Colorize("RetentionVersionID", m.VersionID+" ") 171 } 172 173 msg += m.URLPath 174 return msg 175 } 176 177 // JSON'ified message for scripting. 178 func (m retentionInfoMessageList) JSON() string { 179 if m.Err != nil { 180 m.Status = "failure" 181 } 182 msgBytes, e := json.MarshalIndent(m, "", " ") 183 fatalIf(probe.NewError(e), "Unable to marshal into JSON.") 184 return string(msgBytes) 185 } 186 187 type retentionInfoMessageRecord retentionInfoMessage 188 189 func (m *retentionInfoMessageRecord) SetErr(e error) { 190 m.Err = e 191 } 192 193 func (m *retentionInfoMessageRecord) SetStatus(status string) { 194 m.Status = status 195 } 196 197 func (m *retentionInfoMessageRecord) SetMode(mode minio.RetentionMode) { 198 m.Mode = mode 199 } 200 201 func (m *retentionInfoMessageRecord) SetUntil(until time.Time) { 202 m.Until = until 203 } 204 205 // Colorized message for console printing. 206 func (m retentionInfoMessageRecord) String() string { 207 if m.Err != nil { 208 return console.Colorize("RetentionFailure", fmt.Sprintf("Unable to get object retention on `%s`: %s", m.URLPath, m.Err)) 209 } 210 211 var msg strings.Builder 212 fmt.Fprintf(&msg, "Name : %s\n", console.Colorize("RetentionSuccess", m.URLPath)) 213 214 if m.VersionID != "" { 215 fmt.Fprintf(&msg, "Version : %s\n", console.Colorize("RetentionSuccess", m.VersionID)) 216 } 217 218 fmt.Fprintf(&msg, "Mode : ") 219 if m.Mode == "" { 220 fmt.Fprint(&msg, console.Colorize("RetentionNotFound", "NO RETENTION")) 221 } else { 222 fmt.Fprint(&msg, console.Colorize("RetentionSuccess", m.Mode)) 223 if !m.Until.IsZero() { 224 msg.WriteString(", ") 225 exp := "" 226 now := time.Now() 227 if now.After(m.Until) { 228 prettyDuration := timeDurationToHumanizedDuration(now.Sub(m.Until)).StringShort() 229 exp = console.Colorize("RetentionExpired", "expired "+prettyDuration+" ago") 230 } else { 231 prettyDuration := timeDurationToHumanizedDuration(m.Until.Sub(now)).StringShort() 232 exp = console.Colorize("RetentionSuccess", "expiring in "+prettyDuration) 233 } 234 fmt.Fprint(&msg, exp) 235 } 236 } 237 fmt.Fprint(&msg, "\n") 238 return msg.String() 239 } 240 241 // JSON'ified message for scripting. 242 func (m retentionInfoMessageRecord) JSON() string { 243 if m.Err != nil { 244 m.Status = "failure" 245 } 246 msgBytes, e := json.MarshalIndent(m, "", " ") 247 fatalIf(probe.NewError(e), "Unable to marshal into JSON.") 248 return string(msgBytes) 249 } 250 251 type retentionInfoMsg interface { 252 message 253 SetErr(error) 254 SetStatus(string) 255 SetMode(minio.RetentionMode) 256 SetUntil(time.Time) 257 } 258 259 // Show retention info for a single object or version 260 func infoRetentionSingle(ctx context.Context, alias, url, versionID string, listStyle bool) *probe.Error { 261 newClnt, err := newClientFromAlias(alias, url) 262 if err != nil { 263 return err 264 } 265 266 var msg retentionInfoMsg 267 268 if listStyle { 269 msg = &retentionInfoMessageList{ 270 URLPath: urlJoinPath(alias, url), 271 VersionID: versionID, 272 } 273 } else { 274 msg = &retentionInfoMessageRecord{ 275 URLPath: urlJoinPath(alias, url), 276 VersionID: versionID, 277 } 278 } 279 280 mode, until, err := newClnt.GetObjectRetention(ctx, versionID) 281 if err != nil { 282 errResp := minio.ToErrorResponse(err.ToGoError()) 283 if errResp.Code != "NoSuchObjectLockConfiguration" { 284 if _, ok := err.ToGoError().(ObjectNameEmpty); !ok { 285 msg.SetErr(err.ToGoError()) 286 msg.SetStatus("failure") 287 printMsg(msg) 288 } 289 return err 290 } 291 err = nil 292 } 293 294 msg.SetStatus("success") 295 msg.SetMode(mode) 296 msg.SetUntil(until) 297 298 printMsg(msg) 299 return err 300 } 301 302 // Get Retention for one object/version or many objects within a given prefix. 303 func getRetention(ctx context.Context, target, versionID string, timeRef time.Time, withOlderVersions, isRecursive bool) error { 304 clnt, err := newClient(target) 305 if err != nil { 306 fatalIf(err.Trace(), "Unable to parse the provided url.") 307 } 308 309 // Quit early if urlStr does not point to an S3 server 310 switch clnt.(type) { 311 case *S3Client: 312 default: 313 fatal(errDummy().Trace(), "Retention is supported only for S3 servers.") 314 } 315 316 alias, urlStr, _ := mustExpandAlias(target) 317 if versionID != "" || !isRecursive && !withOlderVersions { 318 err := infoRetentionSingle(ctx, alias, urlStr, versionID, false) 319 if err != nil { 320 if _, ok := err.ToGoError().(ObjectNameEmpty); ok { 321 return showBucketLock(target) 322 } 323 return exitStatus(globalErrorExitStatus) 324 } 325 return nil 326 } 327 328 lstOptions := ListOptions{Recursive: isRecursive, ShowDir: DirNone} 329 if !timeRef.IsZero() { 330 lstOptions.WithOlderVersions = withOlderVersions 331 lstOptions.WithDeleteMarkers = true 332 lstOptions.TimeRef = timeRef 333 } 334 335 var cErr error 336 var atLeastOneObjectOrVersionFound bool 337 338 for content := range clnt.List(ctx, lstOptions) { 339 if content.Err != nil { 340 errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.") 341 cErr = exitStatus(globalErrorExitStatus) // Set the exit status. 342 continue 343 } 344 // The spec does not allow setting retention on delete marker 345 if content.IsDeleteMarker { 346 continue 347 } 348 349 if !isRecursive && alias+getKey(content) != getStandardizedURL(target) { 350 break 351 } 352 353 err := infoRetentionSingle(ctx, alias, content.URL.String(), content.VersionID, true) 354 if err != nil { 355 errorIf(err.Trace(clnt.GetURL().String()), "Invalid URL") 356 cErr = exitStatus(globalErrorExitStatus) 357 continue 358 } 359 360 atLeastOneObjectOrVersionFound = true 361 } 362 363 if !atLeastOneObjectOrVersionFound { 364 errorIf(errDummy().Trace(clnt.GetURL().String()), "Unable to find any object/version to show its retention.") 365 cErr = exitStatus(globalErrorExitStatus) // Set the exit status. 366 } 367 368 return cErr 369 } 370 371 // main for retention info command. 372 func mainRetentionInfo(cliCtx *cli.Context) error { 373 ctx, cancelSetRetention := context.WithCancel(globalContext) 374 defer cancelSetRetention() 375 376 console.SetColor("RetentionSuccess", color.New(color.FgGreen, color.Bold)) 377 console.SetColor("RetentionNotFound", color.New(color.FgYellow)) 378 console.SetColor("RetentionVersionID", color.New(color.FgGreen)) 379 console.SetColor("RetentionExpired", color.New(color.FgRed, color.Bold)) 380 console.SetColor("RetentionFailure", color.New(color.FgYellow)) 381 382 target, versionID, recursive, rewind, withVersions, bucketMode := parseInfoRetentionArgs(cliCtx) 383 384 fatalIfBucketLockNotSupported(ctx, target) 385 386 if bucketMode { 387 return showBucketLock(target) 388 } 389 390 if withVersions && rewind.IsZero() { 391 rewind = time.Now().UTC() 392 } 393 394 return getRetention(ctx, target, versionID, rewind, withVersions, recursive) 395 }