zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/cli/client/discover.go (about)

     1  //go:build search
     2  // +build search
     3  
     4  package client
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  
    10  	distext "github.com/opencontainers/distribution-spec/specs-go/v1/extensions"
    11  
    12  	zerr "zotregistry.dev/zot/errors"
    13  	"zotregistry.dev/zot/pkg/api/constants"
    14  	zcommon "zotregistry.dev/zot/pkg/common"
    15  )
    16  
    17  type field struct {
    18  	Name string `json:"name"`
    19  	Args []struct {
    20  		Name string `json:"name"`
    21  	} `json:"args"`
    22  }
    23  
    24  type schemaList struct {
    25  	Data struct {
    26  		Schema struct {
    27  			QueryType struct {
    28  				Fields []field `json:"fields"`
    29  			} `json:"queryType"` //nolint:tagliatelle // graphQL schema
    30  			Types []typeInfo `json:"types"`
    31  		} `json:"__schema"` //nolint:tagliatelle // graphQL schema
    32  	} `json:"data"`
    33  	Errors []zcommon.ErrorGQL `json:"errors"`
    34  }
    35  
    36  type typeInfo struct {
    37  	Name   string      `json:"name"`
    38  	Fields []typeField `json:"fields"`
    39  }
    40  
    41  type typeField struct {
    42  	Name string `json:"name"`
    43  }
    44  
    45  func containsGQLQueryWithParams(queryList []field, serverGQLTypesList []typeInfo, requiredQueries ...GQLQuery) error {
    46  	serverGQLTypes := map[string][]typeField{}
    47  
    48  	for _, typeInfo := range serverGQLTypesList {
    49  		serverGQLTypes[typeInfo.Name] = typeInfo.Fields
    50  	}
    51  
    52  	for _, reqQuery := range requiredQueries {
    53  		foundQuery := false
    54  
    55  		for _, query := range queryList {
    56  			if query.Name == reqQuery.Name && haveSameArgs(query, reqQuery) {
    57  				foundQuery = true
    58  			}
    59  		}
    60  
    61  		if !foundQuery {
    62  			return fmt.Errorf("%w: %s", zerr.ErrGQLQueryNotSupported, reqQuery.Name)
    63  		}
    64  
    65  		// let's check just the name of the returned type
    66  		returnType := reqQuery.ReturnType.Name
    67  
    68  		// we can next define fields of the returned types and check them recursively
    69  		// for now we will just check the name of the returned type to be known by the server
    70  		_, ok := serverGQLTypes[returnType]
    71  		if !ok {
    72  			return fmt.Errorf("%w: server doesn't support needed type '%s'", zerr.ErrGQLQueryNotSupported, returnType)
    73  		}
    74  	}
    75  
    76  	return nil
    77  }
    78  
    79  func haveSameArgs(query field, reqQuery GQLQuery) bool {
    80  	if len(query.Args) != len(reqQuery.Args) {
    81  		return false
    82  	}
    83  
    84  	for i := range query.Args {
    85  		if query.Args[i].Name != reqQuery.Args[i] {
    86  			return false
    87  		}
    88  	}
    89  
    90  	return true
    91  }
    92  
    93  func CheckExtEndPointQuery(config SearchConfig, requiredQueries ...GQLQuery) error {
    94  	username, password := getUsernameAndPassword(config.User)
    95  	ctx := context.Background()
    96  
    97  	discoverEndPoint, err := combineServerAndEndpointURL(config.ServURL, fmt.Sprintf("%s%s",
    98  		constants.RoutePrefix, constants.ExtOciDiscoverPrefix))
    99  	if err != nil {
   100  		return err
   101  	}
   102  
   103  	discoverResponse := &distext.ExtensionList{}
   104  
   105  	_, err = makeGETRequest(ctx, discoverEndPoint, username, password, config.VerifyTLS,
   106  		config.Debug, &discoverResponse, config.ResultWriter)
   107  	if err != nil {
   108  		return err
   109  	}
   110  
   111  	searchEnabled := false
   112  
   113  	for _, extension := range discoverResponse.Extensions {
   114  		if extension.Name == constants.BaseExtension {
   115  			for _, endpoint := range extension.Endpoints {
   116  				if endpoint == constants.FullSearchPrefix {
   117  					searchEnabled = true
   118  				}
   119  			}
   120  		}
   121  	}
   122  
   123  	if !searchEnabled {
   124  		return fmt.Errorf("%w: search extension gql endpoints not found", zerr.ErrExtensionNotEnabled)
   125  	}
   126  
   127  	searchEndPoint, _ := combineServerAndEndpointURL(config.ServURL, constants.FullSearchPrefix)
   128  
   129  	schemaQuery := `
   130  		{
   131  			__schema() {
   132  				queryType {
   133  					fields {
   134  						name
   135  						args {
   136  							name
   137  						}
   138  						type {
   139  							name
   140  							kind
   141  						}
   142  					}
   143  					__typename
   144  				}
   145  				types {
   146  					name
   147  					fields {
   148  						name
   149  					}
   150  				}
   151  			}
   152  		}`
   153  
   154  	queryResponse := &schemaList{}
   155  
   156  	err = makeGraphQLRequest(ctx, searchEndPoint, schemaQuery, username, password, config.VerifyTLS,
   157  		config.Debug, queryResponse, config.ResultWriter)
   158  	if err != nil {
   159  		return fmt.Errorf("gql query failed: %w", err)
   160  	}
   161  
   162  	if err = checkResultGraphQLQuery(ctx, err, queryResponse.Errors); err != nil {
   163  		return fmt.Errorf("gql query failed: %w", err)
   164  	}
   165  
   166  	return containsGQLQueryWithParams(queryResponse.Data.Schema.QueryType.Fields,
   167  		queryResponse.Data.Schema.Types, requiredQueries...)
   168  }