Skip to content

Commit

Permalink
Add Platform Compatibility Checks to Requirements (#438)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #438

Users can now specify a list of compatible platforms
for a TTP and TTPForge will "fail fast" and report
an incompatibility error if a user attempts to run the TTP
on an incompatible platform - for example:

```
requirements:
  platforms:
    - os: darwin
    - os: linux
```

Reviewed By: nicolagiacchetta, cedowens

Differential Revision: D51459753

fbshipit-source-id: 70c61a5ecc21144f55dd2ddc5e91dfd2da76aa94
  • Loading branch information
d3sch41n authored and facebook-github-bot committed Nov 20, 2023
1 parent ddea151 commit 752ae9c
Show file tree
Hide file tree
Showing 11 changed files with 680 additions and 99 deletions.
76 changes: 76 additions & 0 deletions cmd/run_darwin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//go:build darwin
// +build darwin

/*
Copyright © 2023-present, Meta Platforms, Inc. and affiliates
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package cmd

import (
"bytes"
"path/filepath"

"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestRunDarwin(t *testing.T) {
const testResourcesDir = "test-resources"
const testRepoName = "test-repo"
testConfigFilePath := filepath.Join(testResourcesDir, "test-config.yaml")

testCases := []struct {
name string
description string
args []string
expectedStdout string
wantError bool
}{
{
name: "Check that requirements feature works on Darwin",
description: "Should pass since this file will only be built if we are on darwin",
args: []string{
"-c",
testConfigFilePath,
testRepoName + "//requirements/darwin_only.yaml",
},
expectedStdout: "just a placeholder - we are testing `requirements:`\n",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var stdoutBuf, stderrBuf bytes.Buffer
rc := BuildRootCommand(&TestConfig{
Stdout: &stdoutBuf,
Stderr: &stderrBuf,
})
rc.SetArgs(append([]string{"run"}, tc.args...))
err := rc.Execute()
if tc.wantError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, tc.expectedStdout, stdoutBuf.String())
})
}
}
76 changes: 76 additions & 0 deletions cmd/run_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//go:build linux
// +build linux

/*
Copyright © 2023-present, Meta Platforms, Inc. and affiliates
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package cmd

import (
"bytes"
"path/filepath"

"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestRunLinux(t *testing.T) {
const testResourcesDir = "test-resources"
const testRepoName = "test-repo"
testConfigFilePath := filepath.Join(testResourcesDir, "test-config.yaml")

testCases := []struct {
name string
description string
args []string
expectedStdout string
wantError bool
}{
{
name: "Check that requirements feature works on Linux",
description: "Should pass since this file will only be built if we are on linux",
args: []string{
"-c",
testConfigFilePath,
testRepoName + "//requirements/linux_only.yaml",
},
expectedStdout: "just a placeholder - we are testing `requirements:`\n",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var stdoutBuf, stderrBuf bytes.Buffer
rc := BuildRootCommand(&TestConfig{
Stdout: &stdoutBuf,
Stderr: &stderrBuf,
})
rc.SetArgs(append([]string{"run"}, tc.args...))
err := rc.Execute()
if tc.wantError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, tc.expectedStdout, stdoutBuf.String())
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
name: TTP That Should Only Run on macOS
description: |
This is a test care for the `requirements:` feature that
enforces running only on macOS
requirements:
platforms:
- os: darwin
steps:
- name: placeholder
print_str: just a placeholder - we are testing `requirements:`
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
name: TTP That Should Only Run on Linux
description: |
This is a test care for the `requirements:` feature that
enforces running only on linux
requirements:
platforms:
- os: linux
steps:
- name: placeholder
print_str: just a placeholder - we are testing `requirements:`
107 changes: 107 additions & 0 deletions pkg/blocks/requirements.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
Copyright © 2023-present, Meta Platforms, Inc. and affiliates
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package blocks

import (
"errors"
"fmt"
"os"
"runtime"

"github.com/facebookincubator/ttpforge/pkg/checks"
"github.com/facebookincubator/ttpforge/pkg/logging"
"github.com/facebookincubator/ttpforge/pkg/platforms"
)

// RequirementsConfig specifies the prerequisites that must be
// satisfied before executing a particular TTP.
//
// **Attributes:**
//
// ExpectSuperuser: Whether the TTP assumes superuser privileges
type RequirementsConfig struct {
ExpectSuperuser bool `yaml:"superuser,omitempty"`
Platforms []platforms.Spec `yaml:"platforms,omitempty"`
}

// Validate checks that the requirements section
// is well-formed - it does not actually
// check that the requirements are met.
func (rc *RequirementsConfig) Validate() error {
// nil is valid - it just means don't enforce any
// requirements
if rc == nil {
return nil
}
for _, platform := range rc.Platforms {
if err := platform.Validate(); err != nil {
return err
}
}
return nil
}

// Verify checks that the requirements specified
// in the requirements section are actually satisfied by the environment in
// which the TTP is currently running.
func (rc *RequirementsConfig) Verify(ctx checks.VerificationContext) error {
// simplifies things a bit for callers
if rc == nil {
return nil
}

// check platform compatibility:
// if there are no platforms specified, then we assume
// that the TTP is compatible with all platforms
// (even though it probably isn't, but there
// are a lot of existing TTPs from before this feature
// existed that don't explicitly declare supported platforms)
if len(rc.Platforms) > 0 {
var ttpIsCompatibleWithCurrentPlatform bool
for _, platform := range rc.Platforms {
if platform.IsCompatibleWith(ctx.Platform) {
ttpIsCompatibleWithCurrentPlatform = true
break
}
}
if !ttpIsCompatibleWithCurrentPlatform {
logging.L().Errorf("The current platform %q is not compatible with this TTP", ctx.Platform.String())
logging.L().Errorf("Supported platforms are:")
for _, p := range rc.Platforms {
logging.L().Errorf("\t%v", p.String())
}
return fmt.Errorf("the current platform is not compatible with this TTP")
}
}

// check superuser requirement
if rc.ExpectSuperuser {
if runtime.GOOS == "windows" {
logging.L().Warnf("not enforcing superuser requirement because it is not supported on windows yet")
} else {
if os.Geteuid() != 0 {
err := errors.New("must be root (UID 0) to run this TTP")
return err
}
logging.L().Debug("[+] Running as root")
}
}
return nil
}
100 changes: 100 additions & 0 deletions pkg/blocks/requirements_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
Copyright © 2023-present, Meta Platforms, Inc. and affiliates
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package blocks

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)

func TestRequirements(t *testing.T) {
testCases := []struct {
name string
content string
expectValidateError bool
expectExecuteError bool
expectedRequirements *RequirementsConfig
}{
{
name: "Omitted requirements section",
content: `
name: TestTTP
description: Test description
steps:
- name: hello
print_str: hello world`,
expectedRequirements: nil,
},
{
name: "Valid requirements section",
content: `
name: TestTTP
description: Test description
requirements:
superuser: false
steps:
- name: hello
inline: echo "hello world"`,
expectedRequirements: &RequirementsConfig{
ExpectSuperuser: false,
},
},
{
name: "Invalid requirements section - cannot become root in tests",
content: `
name: TestTTP
description: Test description
requirements:
superuser: true
steps:
- name: hello
print_str: hello world`,
expectExecuteError: true,
expectedRequirements: &RequirementsConfig{
ExpectSuperuser: true,
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var ttp TTP
err := yaml.Unmarshal([]byte(tc.content), &ttp)
require.NoError(t, err)
var ctx TTPExecutionContext
err = ttp.Validate(ctx)
if tc.expectValidateError {
require.Error(t, err)
return
}
require.NoError(t, err)

_, err = ttp.Execute(&ctx)
if tc.expectExecuteError {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}
Loading

0 comments on commit 752ae9c

Please sign in to comment.