Skip to content

Commit

Permalink
[cmd] Implement rootless deprecation messages
Browse files Browse the repository at this point in the history
I have added a system for showing fatal and non-fatal deprecation warnings. It's configurable by command and environment.

If we merge this PR, running a rootless image with any OPA command other than `opa run` will result in a fatal error and exit code 1.

It's possible for users to continue to use the image by unsetting: OPA_DOCKER_IMAGE_TAG=rootless.

`opa run` will show the message, but it's not fatal for this command. This is intended to avoid production disruption.

Signed-off-by: Charlie Egan <[email protected]>
  • Loading branch information
charlieegan3 authored Jul 14, 2023
1 parent cda3bfb commit d584a15
Show file tree
Hide file tree
Showing 5 changed files with 322 additions and 6 deletions.
13 changes: 13 additions & 0 deletions cmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,24 @@ import (
"path"

"github.com/spf13/cobra"

"github.com/open-policy-agent/opa/cmd/internal/deprecation"
)

// RootCommand is the base CLI command that all subcommands are added to.
var RootCommand = &cobra.Command{
Use: path.Base(os.Args[0]),
Short: "Open Policy Agent (OPA)",
Long: "An open source project to policy-enable your service.",
PersistentPreRun: func(cmd *cobra.Command, args []string) {

message, fatal := deprecation.CheckWarnings(os.Environ(), cmd.Use)
if message != "" {
cmd.PrintErr(message)
if fatal {
os.Exit(1)
}
}

},
}
54 changes: 54 additions & 0 deletions cmd/internal/deprecation/rootless.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package deprecation

// TODO: these warnings can be removed when the rootless images are no longer published.

const rootlessWarningMessage = `OPA appears to be running in a deprecated -rootless image.
Since v0.50.0, the default OPA images have been configured to use a non-root
user.
This image will soon cease to be updated. The following images should now be
used instead:
* openpolicyagent/opa:latest and NOT (openpolicyagent/opa:latest-rootless)
* openpolicyagent/opa:edge and NOT (openpolicyagent/opa:edge-rootless)
* openpolicyagent/opa:X.Y.Z and NOT (openpolicyagent/opa:X.Y.Z-rootless)
You can choose to acknowledge and ignore this message by unsetting:
OPA_DOCKER_IMAGE_TAG=rootless
`

// warningRootless is a fatal warning is triggered when the user is running OPA
// in a deprecated rootless image.
var warningRootless = warning{
MatchEnv: func(env []string) bool {
for _, e := range env {
if e == "OPA_DOCKER_IMAGE_TAG=rootless" {
return true
}
}
return false
},
MatchCommand: func(name string) bool {
return name != "run"
},
Fatal: true,
Message: rootlessWarningMessage,
}

// warningRootlessRun is a non-fatal version of the warning reserved for opa run.
// The warning for run is non-fatal to avoid production disruption
var warningRootlessRun = warning{
MatchEnv: func(env []string) bool {
for _, e := range env {
if e == "OPA_DOCKER_IMAGE_TAG=rootless" {
return true
}
}
return false
},
MatchCommand: func(name string) bool {
return name == "run"
},
Fatal: false,
Message: rootlessWarningMessage,
}
92 changes: 92 additions & 0 deletions cmd/internal/deprecation/warning.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package deprecation

import (
"bytes"
"fmt"
"io"
"strings"
)

const width, border = 80, 3
const titleChar, dividerChar = "#", "-"

// warning is a struct which can be used to define deprecation warnings based on
// the environment and command being run.
type warning struct {
MatchEnv func([]string) bool
MatchCommand func(string) bool
Fatal bool
Message string
}

// CheckWarnings runs messageForWarnings with a default set of real warnings.
func CheckWarnings(env []string, command string) (string, bool) {
warnings := []warning{
warningRootless,
warningRootlessRun,
}

return messageForWarnings(warnings, env, command)
}

// messageForWarnings returns an obnoxious banner with the contents of all firing warnings.
// If no warnings fire, it returns an empty string.
// If any warnings are fatal, it returns true for the second return value.
func messageForWarnings(warnings []warning, env []string, command string) (string, bool) {
var messages []string
var fatal bool

for _, w := range warnings {
if w.MatchEnv(env) && w.MatchCommand(command) {
messages = append(messages, w.Message)
if w.Fatal {
fatal = true
}
}
}

buf := bytes.NewBuffer(nil)

if len(messages) == 0 {
return "", false
}

title := "Deprecation Warnings"
if fatal {
title = "Fatal Deprecation Warnings"
}

printFormattedTitle(buf, title)

for i, msg := range messages {
fmt.Fprintln(buf, strings.TrimSpace(msg))
if i < len(messages)-1 {
printFormattedDivider(buf)
}
}

printFormattedTitle(buf, "end "+title)

return buf.String(), fatal
}

func printFormattedTitle(out io.Writer, title string) {
padding := (width - len(title) - border*2) / 2

fmt.Fprintln(out, strings.Repeat(titleChar, width))
fmt.Fprintln(out,
strings.Join(
[]string{
strings.Repeat(titleChar, border),
strings.Repeat(" ", padding), strings.ToUpper(title), strings.Repeat(" ", padding),
strings.Repeat(titleChar, border),
},
"",
),
)
fmt.Fprintln(out, strings.Repeat(titleChar, width))
}

func printFormattedDivider(out io.Writer) {
fmt.Fprintln(out, strings.Repeat(dividerChar, width))
}
163 changes: 163 additions & 0 deletions cmd/internal/deprecation/warning_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package deprecation

import (
"testing"
)

func TestMessageForWarnings(t *testing.T) {
testCases := map[string]struct {
Env []string
Command string
Warnings []warning
ExpectedMessage string
ExpectedFatal bool
}{
"warning that does not fire": {
Env: []string{"OPA_FOOBAR=1"},
Command: "foobar",
Warnings: []warning{
{
MatchEnv: func(env []string) bool {
return false
},
MatchCommand: func(command string) bool {
return false
},
Fatal: true,
Message: "fatal warning",
},
},
ExpectedFatal: false,
ExpectedMessage: "",
},
"warning that fires": {
Env: []string{"OPA_FOOBAR=1"},
Command: "foobar",
Warnings: []warning{
{
MatchEnv: func(env []string) bool {
for _, e := range env {
if e == "OPA_FOOBAR=1" {
return true
}
}
return false
},
MatchCommand: func(command string) bool {
return command == "foobar"
},
Fatal: true,
Message: "fatal warning for foobar",
},
},
ExpectedMessage: `################################################################################
### FATAL DEPRECATION WARNINGS ###
################################################################################
fatal warning for foobar
################################################################################
### END FATAL DEPRECATION WARNINGS ###
################################################################################
`,
ExpectedFatal: true,
},
"two warnings that fire, one fatally": {
Env: []string{"OPA_FOOBAR=1"},
Command: "foobar",
Warnings: []warning{
{
MatchEnv: func(env []string) bool {
for _, e := range env {
if e == "OPA_FOOBAR=1" {
return true
}
}
return false
},
MatchCommand: func(command string) bool {
return command == "foobar"
},
Fatal: true,
Message: "fatal warning for foobar",
},
{
MatchEnv: func(env []string) bool {
return true
},
MatchCommand: func(command string) bool {
return command == "foobar"
},
Fatal: false,
Message: "non fatal warning for foobar",
},
},
ExpectedMessage: `################################################################################
### FATAL DEPRECATION WARNINGS ###
################################################################################
fatal warning for foobar
--------------------------------------------------------------------------------
non fatal warning for foobar
################################################################################
### END FATAL DEPRECATION WARNINGS ###
################################################################################
`,
ExpectedFatal: true,
},
"two warnings that fire, neither fatally": {
Env: []string{"OPA_FOOBAR=1"},
Command: "foobar",
Warnings: []warning{
{
MatchEnv: func(env []string) bool {
for _, e := range env {
if e == "OPA_FOOBAR=1" {
return true
}
}
return false
},
MatchCommand: func(command string) bool {
return command == "foobar"
},
Fatal: false,
Message: "warning for foobar",
},
{
MatchEnv: func(env []string) bool {
return true
},
MatchCommand: func(command string) bool {
return command == "foobar"
},
Fatal: false,
Message: "another warning for foobar",
},
},
ExpectedMessage: `################################################################################
### DEPRECATION WARNINGS ###
################################################################################
warning for foobar
--------------------------------------------------------------------------------
another warning for foobar
################################################################################
### END DEPRECATION WARNINGS ###
################################################################################
`,
ExpectedFatal: false,
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
message, fatal := messageForWarnings(tc.Warnings, tc.Env, tc.Command)

if fatal != tc.ExpectedFatal {
t.Errorf("Expected fatal to be %v but got %v", tc.ExpectedFatal, fatal)
}

if message != tc.ExpectedMessage {
t.Errorf("Expected message\n%s\nbut got\n%s", tc.ExpectedMessage, message)
}

})
}
}
6 changes: 0 additions & 6 deletions runtime/check_user_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package runtime

import (
"os"
"os/user"

"github.com/open-policy-agent/opa/logging"
Expand All @@ -21,9 +20,4 @@ func checkUserPrivileges(logger logging.Logger) {
message := "OPA running with uid or gid 0. Running OPA with root privileges is not recommended."
logger.Warn(message)
}

if os.Getenv("OPA_DOCKER_IMAGE_TAG") == "rootless" {
message := "The -rootless image tag will not be published after OPA v0.52.0."
logger.Error(message)
}
}

0 comments on commit d584a15

Please sign in to comment.