2021-06-16 03:29:04 -04:00
/ *
Copyright 2021 The Kubernetes Authors .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
package main
import (
"bytes"
"encoding/json"
"fmt"
2023-09-01 12:43:28 -04:00
"io/ioutil"
2021-06-16 03:29:04 -04:00
"log"
"os"
"os/exec"
2021-07-05 01:54:27 -04:00
"sort"
2021-06-16 03:29:04 -04:00
"strings"
2023-06-18 01:54:33 -04:00
"github.com/google/go-cmp/cmp"
2021-06-16 03:29:04 -04:00
)
type Unwanted struct {
// things we want to stop referencing
Spec UnwantedSpec ` json:"spec" `
// status of our unwanted dependencies
Status UnwantedStatus ` json:"status" `
}
type UnwantedSpec struct {
// module names we don't want to depend on, mapped to an optional message about why
UnwantedModules map [ string ] string ` json:"unwantedModules" `
}
type UnwantedStatus struct {
2022-10-28 01:07:55 -04:00
// references to modules in the spec.unwantedModules list, based on `go mod graph` content.
2021-07-05 01:54:27 -04:00
// eliminating things from this list is good, and sometimes requires working with upstreams to do so.
2022-10-28 01:07:55 -04:00
UnwantedReferences map [ string ] [ ] string ` json:"unwantedReferences" `
2023-09-01 12:43:28 -04:00
// list of modules in the spec.unwantedModules list which are vendored
UnwantedVendored [ ] string ` json:"unwantedVendored" `
2021-06-16 03:29:04 -04:00
}
// runCommand runs the cmd and returns the combined stdout and stderr, or an
// error if the command failed.
func runCommand ( cmd ... string ) ( string , error ) {
2023-08-02 15:44:25 -04:00
return runCommandInDir ( "" , cmd )
}
func runCommandInDir ( dir string , cmd [ ] string ) ( string , error ) {
c := exec . Command ( cmd [ 0 ] , cmd [ 1 : ] ... )
c . Dir = dir
output , err := c . CombinedOutput ( )
2021-06-16 03:29:04 -04:00
if err != nil {
return "" , fmt . Errorf ( "failed to run %q: %s (%s)" , strings . Join ( cmd , " " ) , err , output )
}
return string ( output ) , nil
}
func readFile ( path string ) ( string , error ) {
2021-11-01 20:54:28 -04:00
content , err := os . ReadFile ( path )
2021-06-16 03:29:04 -04:00
// Convert []byte to string and print to screen
return string ( content ) , err
}
2022-10-28 01:07:55 -04:00
func moduleInSlice ( a module , list [ ] module , matchVersion bool ) bool {
2021-06-16 03:29:04 -04:00
for _ , b := range list {
if b == a {
return true
}
2022-10-28 01:07:55 -04:00
if ! matchVersion && b . name == a . name {
return true
}
2021-06-16 03:29:04 -04:00
}
return false
}
2022-10-28 01:07:55 -04:00
// converts `go mod graph` output modStr into a map of from->[]to references and the main module
func convertToMap ( modStr string ) ( [ ] module , map [ module ] [ ] module ) {
var (
mainModulesList = [ ] module { }
mainModules = map [ module ] bool { }
)
modMap := make ( map [ module ] [ ] module )
2021-06-16 03:29:04 -04:00
for _ , line := range strings . Split ( modStr , "\n" ) {
if len ( line ) == 0 {
continue
}
deps := strings . Split ( line , " " )
if len ( deps ) == 2 {
2022-10-28 01:07:55 -04:00
first := parseModule ( deps [ 0 ] )
second := parseModule ( deps [ 1 ] )
if first . version == "" || first . version == "v0.0.0" {
if ! mainModules [ first ] {
mainModules [ first ] = true
mainModulesList = append ( mainModulesList , first )
}
2021-06-16 03:29:04 -04:00
}
2022-10-28 01:07:55 -04:00
modMap [ first ] = append ( modMap [ first ] , second )
2021-06-16 03:29:04 -04:00
} else {
// skip invalid line
log . Printf ( "!!!invalid line in mod.graph: %s" , line )
continue
}
}
2022-10-28 01:07:55 -04:00
return mainModulesList , modMap
2021-06-16 03:29:04 -04:00
}
2022-10-28 01:07:55 -04:00
// difference returns a-b and b-a as sorted lists
func difference ( a , b [ ] string ) ( [ ] string , [ ] string ) {
2021-07-05 01:54:27 -04:00
aMinusB := map [ string ] bool { }
bMinusA := map [ string ] bool { }
for _ , dependency := range a {
aMinusB [ dependency ] = true
}
for _ , dependency := range b {
if _ , found := aMinusB [ dependency ] ; found {
delete ( aMinusB , dependency )
} else {
bMinusA [ dependency ] = true
}
}
2022-10-28 01:07:55 -04:00
aMinusBList := [ ] string { }
bMinusAList := [ ] string { }
for dependency := range aMinusB {
aMinusBList = append ( aMinusBList , dependency )
}
for dependency := range bMinusA {
bMinusAList = append ( bMinusAList , dependency )
}
sort . Strings ( aMinusBList )
sort . Strings ( bMinusAList )
return aMinusBList , bMinusAList
}
type module struct {
name string
version string
}
func ( m module ) String ( ) string {
if len ( m . version ) == 0 {
return m . name
}
return m . name + "@" + m . version
}
func parseModule ( s string ) module {
if ! strings . Contains ( s , "@" ) {
return module { name : s }
}
parts := strings . SplitN ( s , "@" , 2 )
return module { name : parts [ 0 ] , version : parts [ 1 ] }
2021-07-05 01:54:27 -04:00
}
2021-06-16 03:29:04 -04:00
// option1: dependencyverifier dependencies.json
// it will run `go mod graph` and check it.
func main ( ) {
var modeGraphStr string
var err error
if len ( os . Args ) == 2 {
// run `go mod graph`
modeGraphStr , err = runCommand ( "go" , "mod" , "graph" )
if err != nil {
log . Fatalf ( "Error running 'go mod graph': %s" , err )
}
} else {
2023-08-02 15:44:25 -04:00
log . Fatalf ( "Usage: %s dependencies.json" , os . Args [ 0 ] )
2021-06-16 03:29:04 -04:00
}
dependenciesJSONPath := string ( os . Args [ 1 ] )
dependencies , err := readFile ( dependenciesJSONPath )
if err != nil {
log . Fatalf ( "Error reading dependencies file %s: %s" , dependencies , err )
}
// load Unwanted from json
configFromFile := & Unwanted { }
decoder := json . NewDecoder ( bytes . NewBuffer ( [ ] byte ( dependencies ) ) )
decoder . DisallowUnknownFields ( )
if err := decoder . Decode ( configFromFile ) ; err != nil {
log . Fatalf ( "Error reading dependencies file %s: %s" , dependenciesJSONPath , err )
}
2022-10-28 01:07:55 -04:00
// convert from `go mod graph` to main module and map of from->[]to references
mainModules , moduleGraph := convertToMap ( modeGraphStr )
2023-08-02 15:44:25 -04:00
directDependencies := map [ string ] map [ string ] bool { }
for _ , mainModule := range mainModules {
dir := ""
if mainModule . name != "k8s.io/kubernetes" {
dir = "staging/src/" + mainModule . name
}
listOutput , err := runCommandInDir ( dir , [ ] string { "go" , "list" , "-m" , "-f" , "{{if not .Indirect}}{{if not .Main}}{{.Path}}{{end}}{{end}}" , "all" } )
if err != nil {
log . Fatalf ( "Error running 'go list' for %s: %s" , mainModule . name , err )
}
directDependencies [ mainModule . name ] = map [ string ] bool { }
for _ , directDependency := range strings . Split ( listOutput , "\n" ) {
directDependencies [ mainModule . name ] [ directDependency ] = true
}
}
2022-10-28 01:07:55 -04:00
// gather the effective versions by looking at the versions required by the main modules
effectiveVersions := map [ string ] module { }
for _ , mainModule := range mainModules {
for _ , override := range moduleGraph [ mainModule ] {
if _ , ok := effectiveVersions [ override . name ] ; ! ok {
effectiveVersions [ override . name ] = override
}
}
}
unwantedToReferencers := map [ string ] [ ] module { }
for _ , mainModule := range mainModules {
// visit to find unwanted modules still referenced from the main module
visit ( func ( m module , via [ ] module ) {
if _ , unwanted := configFromFile . Spec . UnwantedModules [ m . name ] ; unwanted {
// this is unwanted, store what is referencing it
referencer := via [ len ( via ) - 1 ]
if ! moduleInSlice ( referencer , unwantedToReferencers [ m . name ] , false ) {
// // uncomment to get a detailed tree of the path that referenced the unwanted dependency
//
// i := 0
// for _, v := range via {
// if v.version != "" && v.version != "v0.0.0" {
// fmt.Println(strings.Repeat(" ", i), v)
// i++
// }
// }
// if i > 0 {
// fmt.Println(strings.Repeat(" ", i+1), m)
// fmt.Println()
// }
unwantedToReferencers [ m . name ] = append ( unwantedToReferencers [ m . name ] , referencer )
}
}
} , mainModule , moduleGraph , effectiveVersions )
}
2021-06-16 03:29:04 -04:00
config := & Unwanted { }
config . Spec . UnwantedModules = configFromFile . Spec . UnwantedModules
2022-10-28 01:07:55 -04:00
for unwanted := range unwantedToReferencers {
if config . Status . UnwantedReferences == nil {
config . Status . UnwantedReferences = map [ string ] [ ] string { }
}
sort . Slice ( unwantedToReferencers [ unwanted ] , func ( i , j int ) bool {
ri := unwantedToReferencers [ unwanted ] [ i ]
rj := unwantedToReferencers [ unwanted ] [ j ]
if ri . name != rj . name {
return ri . name < rj . name
}
return ri . version < rj . version
} )
for _ , referencer := range unwantedToReferencers [ unwanted ] {
// make sure any reference at all shows up as a non-nil status
if config . Status . UnwantedReferences == nil {
config . Status . UnwantedReferences [ unwanted ] = [ ] string { }
}
// record specific names of versioned referents
if referencer . version != "" && referencer . version != "v0.0.0" {
config . Status . UnwantedReferences [ unwanted ] = append ( config . Status . UnwantedReferences [ unwanted ] , referencer . name )
2023-08-02 15:44:25 -04:00
} else if directDependencies [ referencer . name ] [ unwanted ] {
config . Status . UnwantedReferences [ unwanted ] = append ( config . Status . UnwantedReferences [ unwanted ] , referencer . name )
2022-10-28 01:07:55 -04:00
}
}
}
2021-06-16 03:29:04 -04:00
2023-09-01 12:43:28 -04:00
vendorModulesTxt , err := ioutil . ReadFile ( "vendor/modules.txt" )
if err != nil {
log . Fatal ( err )
}
vendoredModules := map [ string ] bool { }
for _ , l := range strings . Split ( string ( vendorModulesTxt ) , "\n" ) {
parts := strings . Split ( l , " " )
if len ( parts ) == 3 && parts [ 0 ] == "#" && strings . HasPrefix ( parts [ 2 ] , "v" ) {
vendoredModules [ parts [ 1 ] ] = true
}
}
config . Status . UnwantedVendored = [ ] string { }
for unwanted := range configFromFile . Spec . UnwantedModules {
if vendoredModules [ unwanted ] {
config . Status . UnwantedVendored = append ( config . Status . UnwantedVendored , unwanted )
}
}
sort . Strings ( config . Status . UnwantedVendored )
2021-07-05 01:54:27 -04:00
needUpdate := false
2022-10-28 01:07:55 -04:00
2021-07-05 01:54:27 -04:00
// Compare unwanted list from unwanted-dependencies.json with current status from `go mod graph`
2022-10-28 01:07:55 -04:00
expected , err := json . MarshalIndent ( configFromFile . Status , "" , " " )
if err != nil {
log . Fatal ( err )
}
actual , err := json . MarshalIndent ( config . Status , "" , " " )
if err != nil {
log . Fatal ( err )
}
if ! bytes . Equal ( expected , actual ) {
log . Printf ( "Expected status of\n%s" , string ( expected ) )
log . Printf ( "Got status of\n%s" , string ( actual ) )
2023-09-01 12:43:28 -04:00
needUpdate = true
log . Print ( "Status diff:\n" , cmp . Diff ( expected , actual ) )
2022-10-28 01:07:55 -04:00
}
for expectedRef , expectedFrom := range configFromFile . Status . UnwantedReferences {
actualFrom , ok := config . Status . UnwantedReferences [ expectedRef ]
if ! ok {
// disappeared entirely
log . Printf ( "Good news! Unwanted dependency %q is no longer referenced. Remove status.unwantedReferences[%q] in %s to ensure it doesn't get reintroduced." , expectedRef , expectedRef , dependenciesJSONPath )
needUpdate = true
continue
}
removedReferences , unwantedReferences := difference ( expectedFrom , actualFrom )
if len ( removedReferences ) > 0 {
log . Printf ( "Good news! Unwanted module %q dropped the following dependants:" , expectedRef )
for _ , reference := range removedReferences {
log . Printf ( " %s" , reference )
}
log . Printf ( "!!! Remove those from status.unwantedReferences[%q] in %s to ensure they don't get reintroduced." , expectedRef , dependenciesJSONPath )
needUpdate = true
}
if len ( unwantedReferences ) > 0 {
log . Printf ( "Unwanted module %q marked in %s is referenced by new dependants:" , expectedRef , dependenciesJSONPath )
for _ , reference := range unwantedReferences {
log . Printf ( " %s" , reference )
}
log . Printf ( "!!! Avoid updating referencing modules to versions that reintroduce use of unwanted dependencies\n" )
needUpdate = true
2021-07-05 01:54:27 -04:00
}
}
2022-10-28 01:07:55 -04:00
for actualRef , actualFrom := range config . Status . UnwantedReferences {
if _ , expected := configFromFile . Status . UnwantedReferences [ actualRef ] ; expected {
// expected, already ensured referencers were equal in the first loop
continue
}
log . Printf ( "Unwanted module %q marked in %s is referenced" , actualRef , dependenciesJSONPath )
for _ , reference := range actualFrom {
log . Printf ( " %s" , reference )
2021-07-05 01:54:27 -04:00
}
log . Printf ( "!!! Avoid updating referencing modules to versions that reintroduce use of unwanted dependencies\n" )
needUpdate = true
}
2022-10-28 01:07:55 -04:00
2023-09-01 12:43:28 -04:00
removedVendored , addedVendored := difference ( configFromFile . Status . UnwantedVendored , config . Status . UnwantedVendored )
if len ( removedVendored ) > 0 {
log . Printf ( "Good news! Unwanted modules are no longer vendered: %q" , removedVendored )
log . Printf ( "!!! Remove those from status.unwantedVendored in %s to ensure they don't get reintroduced." , dependenciesJSONPath )
needUpdate = true
}
if len ( addedVendored ) > 0 {
log . Printf ( "Unwanted modules are newly vendored: %q" , addedVendored )
log . Printf ( "!!! Avoid updates that increase vendoring of unwanted dependencies\n" )
needUpdate = true
}
2021-07-05 01:54:27 -04:00
if needUpdate {
2021-06-16 03:29:04 -04:00
os . Exit ( 1 )
}
}
2022-10-28 01:07:55 -04:00
func visit ( visitor func ( m module , via [ ] module ) , main module , references map [ module ] [ ] module , effectiveVersions map [ string ] module ) {
doVisit ( visitor , main , nil , map [ module ] bool { } , references , effectiveVersions )
}
func doVisit ( visitor func ( m module , via [ ] module ) , from module , via [ ] module , visited map [ module ] bool , references map [ module ] [ ] module , effectiveVersions map [ string ] module ) {
visitor ( from , via )
via = append ( via , from )
if visited [ from ] {
return
}
for _ , to := range references [ from ] {
// switch to the effective version of this dependency
if override , ok := effectiveVersions [ to . name ] ; ok {
to = override
}
// recurse unless we've already visited this module in this traversal
if ! moduleInSlice ( to , via , false ) {
doVisit ( visitor , to , via , visited , references , effectiveVersions )
}
}
visited [ from ] = true
}